feat(kernel): analyst entity grounding (role-reliability §3)

Local analysts hallucinate file paths the same as planners. A stage can now
opt in with `ground_references = true`: after it produces its brief, every
workspace-relative file path the brief named is checked for existence, and a
path that doesn't exist fails the stage (retryable) with the misses fed back
("Reference only files you have read") so the model re-grounds.

Grounding probes the filesystem directly rather than the repo map — the map
is recency-ranked and depth/extension-limited, not a complete existence
oracle, so "absent from the map" can't be a hard failure. Existence is a
nondeterministic observation, so the result is recorded as a
BriefGroundingCheckedEvent (invariant #9); replay reads it back and never
re-probes.

- BriefGroundingCheckedEvent + GroundingReference, registered in eventModule
- BriefReferenceExtractor: pure, deterministic pull of relative file-path
  references from a brief artifact JSON (needs a '/' and a dotted extension;
  skips modules, bare names, URLs, absolute/parent paths — so coarse
  "subsystem" mentions the brief is allowed to make aren't false-flagged)
- SessionOrchestrator.groundBriefReferences on the post-verifyProduces path,
  gated by stage metadata; uses the existing worldProbe
- TomlWorkflowLoader `ground_references` → metadata; role_pipeline analyst
  opts in

Scoped to file references with a '/' + extension (not bare basenames, which
a depth-limited probe can't resolve), so a flagged miss is high-confidence.
The symbol-grounding half of §3 is left for a real symbol index.
This commit is contained in:
2026-06-13 16:20:30 +04:00
parent 4e5e4e56ea
commit b050dc4e83
9 changed files with 263 additions and 1 deletions
@@ -46,6 +46,7 @@ private data class StageSection(
@param:JsonProperty("max_retries") val maxRetries: Int = 3,
@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,
)
// condition fields flattened into the transition row
@@ -119,6 +120,7 @@ class TomlWorkflowLoader(
s.systemPrompt?.let { put("systemPrompt", resolvePromptPath(it, workflowDir)) }
if (s.requiresApproval) put("requiresApproval", "true")
if (s.injectArtifactKinds) put("injectArtifactKinds", "true")
if (s.groundReferences) put("groundReferences", "true")
},
)
}
@@ -155,4 +155,22 @@ class TomlWorkflowLoaderTest {
val collectStage = graph.stages[graph.start]!!
assertEquals(emptyList(), collectStage.writeManifest)
}
@Test
fun `ground_references true maps to metadata key`() {
val toml = validToml.replace(
"prompt = \"prompts/collect.md\"",
"prompt = \"prompts/collect.md\"\nground_references = true",
)
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(toml) }
val graph = loader.load(path)
assertEquals("true", graph.stages[graph.start]!!.metadata["groundReferences"])
}
@Test
fun `ground_references absent means no metadata key`() {
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(validToml) }
val graph = loader.load(path)
assertEquals(null, graph.stages[graph.start]!!.metadata["groundReferences"])
}
}