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:
2026-06-21 10:45:43 +00:00
parent 04d7e26482
commit c616982b7b
14 changed files with 269 additions and 1 deletions
@@ -15,6 +15,7 @@ import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.StageId
import com.correx.core.sessions.BoundAgentInstructions
import com.correx.core.sessions.BoundProjectProfile
import com.correx.core.transitions.graph.WorkflowGraph
import java.util.UUID
@@ -153,3 +154,17 @@ fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
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,
)
}
@@ -399,6 +399,8 @@ abstract class SessionOrchestrator(
} ?: emptyList()
val projectProfileEntries = session.state.boundProjectProfile
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
val agentInstructionsEntries = session.state.boundAgentInstructions
?.let { listOf(buildAgentInstructionsEntry(it)) } ?: emptyList()
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
?.let { listOf(it) } ?: emptyList()
val vocabularyEntries = artifactKindRegistry
@@ -411,7 +413,8 @@ abstract class SessionOrchestrator(
.mapNotNull { it.payload as? StaticFindingsRecordedEvent }
.flatMap { it.findings }
var accumulatedEntries = excludeStaticFindingsFromReview(
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
systemPrompt + profileEntries + projectProfileEntries + agentInstructionsEntries +
journalEntries + repoMapEntries +
needsEntries + schemaEntries + vocabularyEntries + promptEntries + steeringEntries +
clarificationEntries + retryFeedbackEntries,
recordedStaticFindings,