feat(context): inject CLAUDE.md / AGENTS.md as L0 standing context
On session start, discover CLAUDE.md and AGENTS.md at the bound workspace root and inject their (concatenated, header-labeled) contents as an L0 system context entry for every stage — mirroring the .correx/project.toml ProjectProfile path end to end. Recorded as AgentInstructionsBoundEvent (invariant #9) so replay reads the recorded fact, not the live file; folded into SessionState.boundAgentInstructions and rendered by buildAgentInstructionsEntry right after the project profile. AgentInstructionsLoader reads root-only, both files if present. Bound via ServerModule.bindAgentInstructions alongside bindProjectProfile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,11 @@ import com.correx.core.approvals.ApprovalProjector
|
|||||||
import com.correx.core.approvals.DefaultApprovalReducer
|
import com.correx.core.approvals.DefaultApprovalReducer
|
||||||
import com.correx.core.approvals.DefaultApprovalRepository
|
import com.correx.core.approvals.DefaultApprovalRepository
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
|
import com.correx.core.config.AgentInstructionsLoader
|
||||||
import com.correx.core.config.OperatorProfile
|
import com.correx.core.config.OperatorProfile
|
||||||
import com.correx.core.config.ProjectProfileLoader
|
import com.correx.core.config.ProjectProfileLoader
|
||||||
import com.correx.apps.server.memory.ArchitectContradictionChecker
|
import com.correx.apps.server.memory.ArchitectContradictionChecker
|
||||||
|
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.events.ArtifactContentStoredEvent
|
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||||
@@ -322,6 +324,7 @@ class ServerModule(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
bindProjectProfile(sessionId)
|
bindProjectProfile(sessionId)
|
||||||
|
bindAgentInstructions(sessionId)
|
||||||
runCatching {
|
runCatching {
|
||||||
val result = orchestrator.run(sessionId, graph, sessionConfig)
|
val result = orchestrator.run(sessionId, graph, sessionConfig)
|
||||||
freestyleHandoff(sessionId, graph, result)
|
freestyleHandoff(sessionId, graph, result)
|
||||||
@@ -405,6 +408,37 @@ class ServerModule(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind standing agent instructions (CLAUDE.md / AGENTS.md at the workspace root) as an
|
||||||
|
* event so stages, router chat triage, and replay read the recorded snapshot, never the
|
||||||
|
* live file (invariants #8/#9). Mirrors [bindProjectProfile].
|
||||||
|
*/
|
||||||
|
suspend fun bindAgentInstructions(sessionId: SessionId) {
|
||||||
|
val workspaceRoot = runCatching {
|
||||||
|
sessionRepository.getSession(sessionId).state.boundWorkspace?.workspaceRoot
|
||||||
|
}.getOrNull() ?: projectMemory?.repoRoot() ?: return
|
||||||
|
val instructions = withContext(Dispatchers.IO) { AgentInstructionsLoader.load(workspaceRoot) }
|
||||||
|
if (instructions.isEmpty()) return
|
||||||
|
eventStore.append(
|
||||||
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = EventId(java.util.UUID.randomUUID().toString()),
|
||||||
|
sessionId = sessionId,
|
||||||
|
timestamp = Clock.System.now(),
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null,
|
||||||
|
),
|
||||||
|
payload = AgentInstructionsBoundEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
workspaceRoot = workspaceRoot,
|
||||||
|
sources = instructions.sources,
|
||||||
|
content = instructions.content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phase-2 handoff for freestyle sessions, shared by every launcher that can finish a
|
* Phase-2 handoff for freestyle sessions, shared by every launcher that can finish a
|
||||||
* planning graph (fresh run and all resume paths). Locks the plan and executes it,
|
* planning graph (fresh run and all resume paths). Locks the plan and executes it,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.correx.core.config
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standing agent instructions discovered at the bound workspace root (`CLAUDE.md` and/or
|
||||||
|
* `AGENTS.md`). Unlike the curated `.correx/project.toml` ProjectProfile, these are the
|
||||||
|
* free-form markdown briefs the operator already maintains for coding agents: injected as
|
||||||
|
* L0 for every stage of every session bound to the workspace.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class AgentInstructions(
|
||||||
|
val sources: List<String> = emptyList(),
|
||||||
|
val content: String = "",
|
||||||
|
) {
|
||||||
|
fun isEmpty(): Boolean = sources.isEmpty() && content.isBlank()
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.correx.core.config
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers standing agent instructions (`CLAUDE.md`, `AGENTS.md`) at the bound workspace
|
||||||
|
* root only — no parent walk, no nested lookup. Mirrors [ProjectProfileLoader]: the loaded
|
||||||
|
* snapshot is bound as an event so replay reads the recorded fact, never the live file.
|
||||||
|
*/
|
||||||
|
object AgentInstructionsLoader {
|
||||||
|
private val FILE_NAMES = listOf("CLAUDE.md", "AGENTS.md")
|
||||||
|
|
||||||
|
fun load(workspaceRoot: String): AgentInstructions {
|
||||||
|
val sections = mutableListOf<String>()
|
||||||
|
val sources = mutableListOf<String>()
|
||||||
|
for (name in FILE_NAMES) {
|
||||||
|
val contents = readIfPresent(Paths.get(workspaceRoot, name))
|
||||||
|
if (contents == null || contents.isBlank()) continue
|
||||||
|
sources.add(name)
|
||||||
|
sections.add("# $name\n\n${contents.trim()}")
|
||||||
|
}
|
||||||
|
if (sections.isEmpty()) return AgentInstructions()
|
||||||
|
return AgentInstructions(sources = sources, content = sections.joinToString("\n\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readIfPresent(path: Path): String? {
|
||||||
|
if (!Files.exists(path)) return null
|
||||||
|
return runCatching { Files.readString(path) }.getOrElse { e ->
|
||||||
|
System.err.println("Warning: Failed to read agent instructions at $path: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.correx.core.config
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class AgentInstructionsLoaderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `both files present yields both sections and sources`(@TempDir root: Path) {
|
||||||
|
Files.writeString(root.resolve("CLAUDE.md"), "Claude rules")
|
||||||
|
Files.writeString(root.resolve("AGENTS.md"), "Agents rules")
|
||||||
|
|
||||||
|
val loaded = AgentInstructionsLoader.load(root.toString())
|
||||||
|
|
||||||
|
assertEquals(listOf("CLAUDE.md", "AGENTS.md"), loaded.sources)
|
||||||
|
assertTrue(loaded.content.contains("# CLAUDE.md"), "content: ${loaded.content}")
|
||||||
|
assertTrue(loaded.content.contains("Claude rules"), "content: ${loaded.content}")
|
||||||
|
assertTrue(loaded.content.contains("# AGENTS.md"), "content: ${loaded.content}")
|
||||||
|
assertTrue(loaded.content.contains("Agents rules"), "content: ${loaded.content}")
|
||||||
|
assertFalse(loaded.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `only one file present yields that one`(@TempDir root: Path) {
|
||||||
|
Files.writeString(root.resolve("AGENTS.md"), "Agents only")
|
||||||
|
|
||||||
|
val loaded = AgentInstructionsLoader.load(root.toString())
|
||||||
|
|
||||||
|
assertEquals(listOf("AGENTS.md"), loaded.sources)
|
||||||
|
assertTrue(loaded.content.contains("# AGENTS.md"), "content: ${loaded.content}")
|
||||||
|
assertTrue(loaded.content.contains("Agents only"), "content: ${loaded.content}")
|
||||||
|
assertFalse(loaded.content.contains("CLAUDE.md"), "content: ${loaded.content}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `neither file present is empty`(@TempDir root: Path) {
|
||||||
|
val loaded = AgentInstructionsLoader.load(root.toString())
|
||||||
|
|
||||||
|
assertTrue(loaded.isEmpty())
|
||||||
|
assertEquals(AgentInstructions(), loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank file is skipped`(@TempDir root: Path) {
|
||||||
|
Files.writeString(root.resolve("CLAUDE.md"), " \n ")
|
||||||
|
Files.writeString(root.resolve("AGENTS.md"), "Real content")
|
||||||
|
|
||||||
|
val loaded = AgentInstructionsLoader.load(root.toString())
|
||||||
|
|
||||||
|
assertEquals(listOf("AGENTS.md"), loaded.sources)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,3 +37,12 @@ data class ProjectProfileBoundEvent(
|
|||||||
val conventions: List<String>,
|
val conventions: List<String>,
|
||||||
val commands: Map<String, String>,
|
val commands: Map<String, String>,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("AgentInstructionsBound")
|
||||||
|
data class AgentInstructionsBoundEvent(
|
||||||
|
val sessionId: SessionId,
|
||||||
|
val workspaceRoot: String,
|
||||||
|
val sources: List<String>, // file names found, e.g. ["CLAUDE.md","AGENTS.md"]
|
||||||
|
val content: String, // concatenated, header-labeled instructions
|
||||||
|
) : EventPayload
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.core.events.serialization
|
package com.correx.core.events.serialization
|
||||||
|
|
||||||
|
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||||
import com.correx.core.events.events.ApprovalGrantCreatedEvent
|
import com.correx.core.events.events.ApprovalGrantCreatedEvent
|
||||||
import com.correx.core.events.events.ApprovalGrantExpiredEvent
|
import com.correx.core.events.events.ApprovalGrantExpiredEvent
|
||||||
@@ -126,6 +127,7 @@ val eventModule = SerializersModule {
|
|||||||
subclass(SessionWorkspaceBoundEvent::class)
|
subclass(SessionWorkspaceBoundEvent::class)
|
||||||
subclass(OperatorProfileBoundEvent::class)
|
subclass(OperatorProfileBoundEvent::class)
|
||||||
subclass(ProjectProfileBoundEvent::class)
|
subclass(ProjectProfileBoundEvent::class)
|
||||||
|
subclass(AgentInstructionsBoundEvent::class)
|
||||||
subclass(L3MemoryRetrievedEvent::class)
|
subclass(L3MemoryRetrievedEvent::class)
|
||||||
subclass(ContextTruncatedEvent::class)
|
subclass(ContextTruncatedEvent::class)
|
||||||
subclass(ExecutionPlanLockedEvent::class)
|
subclass(ExecutionPlanLockedEvent::class)
|
||||||
|
|||||||
+15
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.core.events.serialization
|
package com.correx.core.events.serialization
|
||||||
|
|
||||||
|
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||||
import com.correx.core.events.events.EventPayload
|
import com.correx.core.events.events.EventPayload
|
||||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
@@ -24,6 +25,20 @@ class ProjectProfileBoundEventSerializationTest {
|
|||||||
assertEquals(sample, decoded)
|
assertEquals(sample, decoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AgentInstructionsBoundEvent round-trips as polymorphic EventPayload`() {
|
||||||
|
val sample: EventPayload = AgentInstructionsBoundEvent(
|
||||||
|
sessionId = SessionId("s1"),
|
||||||
|
workspaceRoot = "/repo",
|
||||||
|
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||||
|
content = "# CLAUDE.md\n\nbe careful\n\n# AGENTS.md\n\nuse tools",
|
||||||
|
)
|
||||||
|
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||||
|
assertTrue(encoded.contains("\"type\":\"AgentInstructionsBound\""), "SerialName must be present: $encoded")
|
||||||
|
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||||
|
assertEquals(sample, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `round-trips with empty fields`() {
|
fun `round-trips with empty fields`() {
|
||||||
val sample: EventPayload = ProjectProfileBoundEvent(
|
val sample: EventPayload = ProjectProfileBoundEvent(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.correx.core.events.events.StoredEvent
|
|||||||
import com.correx.core.events.types.ArtifactId
|
import com.correx.core.events.types.ArtifactId
|
||||||
import com.correx.core.events.types.ContextEntryId
|
import com.correx.core.events.types.ContextEntryId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
|
import com.correx.core.sessions.BoundAgentInstructions
|
||||||
import com.correx.core.sessions.BoundProjectProfile
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
import com.correx.core.transitions.graph.WorkflowGraph
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -153,3 +154,17 @@ fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
|
|||||||
role = EntryRole.SYSTEM,
|
role = EntryRole.SYSTEM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Renders bound CLAUDE.md / AGENTS.md instructions as a single L0 system entry. */
|
||||||
|
fun buildAgentInstructionsEntry(instructions: BoundAgentInstructions): ContextEntry {
|
||||||
|
val content = instructions.content
|
||||||
|
return ContextEntry(
|
||||||
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
|
layer = ContextLayer.L0,
|
||||||
|
content = content,
|
||||||
|
sourceType = "agentInstructions",
|
||||||
|
sourceId = "agent-instructions",
|
||||||
|
tokenEstimate = content.length / 4,
|
||||||
|
role = EntryRole.SYSTEM,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -399,6 +399,8 @@ abstract class SessionOrchestrator(
|
|||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
val projectProfileEntries = session.state.boundProjectProfile
|
val projectProfileEntries = session.state.boundProjectProfile
|
||||||
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
||||||
|
val agentInstructionsEntries = session.state.boundAgentInstructions
|
||||||
|
?.let { listOf(buildAgentInstructionsEntry(it)) } ?: emptyList()
|
||||||
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
|
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
|
||||||
?.let { listOf(it) } ?: emptyList()
|
?.let { listOf(it) } ?: emptyList()
|
||||||
val vocabularyEntries = artifactKindRegistry
|
val vocabularyEntries = artifactKindRegistry
|
||||||
@@ -411,7 +413,8 @@ abstract class SessionOrchestrator(
|
|||||||
.mapNotNull { it.payload as? StaticFindingsRecordedEvent }
|
.mapNotNull { it.payload as? StaticFindingsRecordedEvent }
|
||||||
.flatMap { it.findings }
|
.flatMap { it.findings }
|
||||||
var accumulatedEntries = excludeStaticFindingsFromReview(
|
var accumulatedEntries = excludeStaticFindingsFromReview(
|
||||||
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
systemPrompt + profileEntries + projectProfileEntries + agentInstructionsEntries +
|
||||||
|
journalEntries + repoMapEntries +
|
||||||
needsEntries + schemaEntries + vocabularyEntries + promptEntries + steeringEntries +
|
needsEntries + schemaEntries + vocabularyEntries + promptEntries + steeringEntries +
|
||||||
clarificationEntries + retryFeedbackEntries,
|
clarificationEntries + retryFeedbackEntries,
|
||||||
recordedStaticFindings,
|
recordedStaticFindings,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.correx.core.sessions
|
||||||
|
|
||||||
|
/** Snapshot of standing agent instructions (CLAUDE.md / AGENTS.md) bound at session start. */
|
||||||
|
data class BoundAgentInstructions(
|
||||||
|
val sources: List<String>,
|
||||||
|
val content: String,
|
||||||
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.core.sessions
|
package com.correx.core.sessions
|
||||||
|
|
||||||
|
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||||
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||||
@@ -61,6 +62,14 @@ class DefaultSessionReducer : SessionReducer {
|
|||||||
else -> state.boundProjectProfile
|
else -> state.boundProjectProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val boundAgentInstructions = when (payload) {
|
||||||
|
is AgentInstructionsBoundEvent -> BoundAgentInstructions(
|
||||||
|
sources = payload.sources,
|
||||||
|
content = payload.content,
|
||||||
|
)
|
||||||
|
else -> state.boundAgentInstructions
|
||||||
|
}
|
||||||
|
|
||||||
return state.copy(
|
return state.copy(
|
||||||
status = newStatus,
|
status = newStatus,
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
@@ -68,6 +77,7 @@ class DefaultSessionReducer : SessionReducer {
|
|||||||
boundWorkspace = boundWorkspace,
|
boundWorkspace = boundWorkspace,
|
||||||
boundProfile = boundProfile,
|
boundProfile = boundProfile,
|
||||||
boundProjectProfile = boundProjectProfile,
|
boundProjectProfile = boundProjectProfile,
|
||||||
|
boundAgentInstructions = boundAgentInstructions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ data class SessionState(
|
|||||||
val boundWorkspace: BoundWorkspace? = null,
|
val boundWorkspace: BoundWorkspace? = null,
|
||||||
val boundProfile: BoundProfile? = null,
|
val boundProfile: BoundProfile? = null,
|
||||||
val boundProjectProfile: BoundProjectProfile? = null,
|
val boundProjectProfile: BoundProjectProfile? = null,
|
||||||
|
val boundAgentInstructions: BoundAgentInstructions? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import com.correx.core.events.types.SessionId
|
|||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.TransitionId
|
import com.correx.core.events.types.TransitionId
|
||||||
import com.correx.core.events.events.RepoKnowledgeHit
|
import com.correx.core.events.events.RepoKnowledgeHit
|
||||||
|
import com.correx.core.kernel.orchestration.buildAgentInstructionsEntry
|
||||||
import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry
|
import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry
|
||||||
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
||||||
import com.correx.core.kernel.orchestration.buildRelevantFilesEntry
|
import com.correx.core.kernel.orchestration.buildRelevantFilesEntry
|
||||||
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
||||||
import com.correx.core.kernel.orchestration.criticArtifactIds
|
import com.correx.core.kernel.orchestration.criticArtifactIds
|
||||||
|
import com.correx.core.sessions.BoundAgentInstructions
|
||||||
import com.correx.core.sessions.BoundProjectProfile
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
import com.correx.core.transitions.graph.StageConfig
|
import com.correx.core.transitions.graph.StageConfig
|
||||||
import com.correx.core.transitions.graph.TransitionEdge
|
import com.correx.core.transitions.graph.TransitionEdge
|
||||||
@@ -87,6 +89,21 @@ class ContextFeedbackTest {
|
|||||||
assertEquals("projectProfile", entry.sourceType)
|
assertEquals("projectProfile", entry.sourceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `agent instructions render as single L0 entry`() {
|
||||||
|
val entry = buildAgentInstructionsEntry(
|
||||||
|
BoundAgentInstructions(
|
||||||
|
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||||
|
content = "# CLAUDE.md\n\nbe careful\n\n# AGENTS.md\n\nuse tools",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(ContextLayer.L0, entry.layer)
|
||||||
|
assertEquals(EntryRole.SYSTEM, entry.role)
|
||||||
|
assertEquals("agentInstructions", entry.sourceType)
|
||||||
|
assertTrue(entry.content.contains("# CLAUDE.md"), "content: ${entry.content}")
|
||||||
|
assertTrue(entry.content.contains("# AGENTS.md"), "content: ${entry.content}")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `criticArtifactIds resolves the from-stage produces on a back-edge`() {
|
fun `criticArtifactIds resolves the from-stage produces on a back-edge`() {
|
||||||
val graph = graphWith(
|
val graph = graphWith(
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import com.correx.core.events.events.WorkflowStartedEvent
|
|||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.TransitionId
|
import com.correx.core.events.types.TransitionId
|
||||||
|
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||||
|
import com.correx.core.sessions.BoundAgentInstructions
|
||||||
import com.correx.core.sessions.BoundProjectProfile
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
import com.correx.core.sessions.BoundWorkspace
|
import com.correx.core.sessions.BoundWorkspace
|
||||||
import com.correx.core.sessions.DefaultSessionReducer
|
import com.correx.core.sessions.DefaultSessionReducer
|
||||||
@@ -327,6 +329,50 @@ class DefaultSessionReducerTest {
|
|||||||
assertEquals("kernel project", result.boundProjectProfile?.about)
|
assertEquals("kernel project", result.boundProjectProfile?.about)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AgentInstructionsBoundEvent reduces into boundAgentInstructions`() {
|
||||||
|
val result = reducer.reduce(
|
||||||
|
state = initialState(),
|
||||||
|
event = stored(
|
||||||
|
sessionId = sessionId,
|
||||||
|
payload = AgentInstructionsBoundEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
workspaceRoot = "/repo",
|
||||||
|
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||||
|
content = "# CLAUDE.md\n\nbe careful",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
BoundAgentInstructions(
|
||||||
|
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||||
|
content = "# CLAUDE.md\n\nbe careful",
|
||||||
|
),
|
||||||
|
result.boundAgentInstructions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `boundAgentInstructions is preserved by subsequent unrelated events`() {
|
||||||
|
val withInstructions = initialState().copy(
|
||||||
|
boundAgentInstructions = BoundAgentInstructions(
|
||||||
|
sources = listOf("CLAUDE.md"),
|
||||||
|
content = "# CLAUDE.md\n\nrules",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = reducer.reduce(
|
||||||
|
state = withInstructions,
|
||||||
|
event = stored(
|
||||||
|
sessionId = sessionId,
|
||||||
|
payload = WorkflowStartedEvent(sessionId, workflowId = "wf", startStageId = StageId("s1")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("CLAUDE.md"), result.boundAgentInstructions?.sources)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initialState() =
|
private fun initialState() =
|
||||||
SessionState(
|
SessionState(
|
||||||
status = SessionStatus.CREATED
|
status = SessionStatus.CREATED
|
||||||
|
|||||||
Reference in New Issue
Block a user