feat(kernel): brief echo-back gate (plan-pipeline-addenda A1)

New brief_echo stage before the planner in role_pipeline: the model restates
the analyst brief as a structured artifact, and a deterministic gate
(checkBriefEcho, opt-in via brief_echo=true) diffs the restatement against the
original brief. On divergence — dropped requirements (Jaccard coverage < 0.34)
or hallucinated files — it emits BriefEchoMismatchEvent and fails the stage
retryably, so a misread brief never reaches plan generation.

The diff (BriefEchoDiff) is a pure function of two recorded artifacts, so it is
replay-safe and records no observation; the event fires only on mismatch, for
audit. Symbols are recorded but non-blocking in v1. Mirrors the groundBrief-
References gate. role_pipeline only; freestyle_planning wiring is a follow-up.

Runtime install (like research): copy brief_echo.json + brief_echo.md and the
[[artifacts]] block into ~/.config/correx.
This commit is contained in:
2026-06-14 19:43:15 +04:00
parent 40245583e5
commit bcc59d2164
12 changed files with 520 additions and 7 deletions
@@ -47,6 +47,7 @@ private data class StageSection(
@param:JsonProperty("requires_approval") val requiresApproval: Boolean = false,
@param:JsonProperty("inject_artifact_kinds") val injectArtifactKinds: Boolean = false,
@param:JsonProperty("ground_references") val groundReferences: Boolean = false,
@param:JsonProperty("brief_echo") val briefEcho: Boolean = false,
)
// condition fields flattened into the transition row
@@ -121,6 +122,7 @@ class TomlWorkflowLoader(
if (s.requiresApproval) put("requiresApproval", "true")
if (s.injectArtifactKinds) put("injectArtifactKinds", "true")
if (s.groundReferences) put("groundReferences", "true")
if (s.briefEcho) put("briefEcho", "true")
},
)
}
@@ -29,6 +29,7 @@ class RolePipelineWorkflowTest {
private val registry = DefaultArtifactKindRegistry().apply {
register(llmKind("analysis"))
register(llmKind("design"))
register(llmKind("brief_echo"))
register(llmKind("impl_plan"))
register(llmKind("review_report"))
}
@@ -53,10 +54,10 @@ class RolePipelineWorkflowTest {
val graph = TomlWorkflowLoader(registry).load(path!!)
assertEquals(
setOf("analyst", "architect", "planner", "implementer", "reviewer"),
setOf("analyst", "architect", "brief_echo", "planner", "implementer", "reviewer"),
graph.stages.keys.map { it.value }.toSet(),
)
assertEquals(6, graph.transitions.size)
assertEquals(7, graph.transitions.size)
// Forward chaining via needs.
assertTrue(graph.stages[StageId("reviewer")]!!.needs.map { it.value }.containsAll(listOf("patch", "impl_plan")))