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
@@ -37,3 +37,12 @@ data class ProjectProfileBoundEvent(
val conventions: List<String>,
val commands: Map<String, String>,
) : 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
import com.correx.core.events.events.AgentInstructionsBoundEvent
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
@@ -126,6 +127,7 @@ val eventModule = SerializersModule {
subclass(SessionWorkspaceBoundEvent::class)
subclass(OperatorProfileBoundEvent::class)
subclass(ProjectProfileBoundEvent::class)
subclass(AgentInstructionsBoundEvent::class)
subclass(L3MemoryRetrievedEvent::class)
subclass(ContextTruncatedEvent::class)
subclass(ExecutionPlanLockedEvent::class)
@@ -1,5 +1,6 @@
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.ProjectProfileBoundEvent
import com.correx.core.events.types.SessionId
@@ -24,6 +25,20 @@ class ProjectProfileBoundEventSerializationTest {
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
fun `round-trips with empty fields`() {
val sample: EventPayload = ProjectProfileBoundEvent(