kernel: deterministic static_check stage (§B-§5) + critique-outcome producer (§B-§6)
Two reviewer-reliability tracks, both deterministic and unit-verified (no model/network). §B-§5 — static_check stage seam. The StaticAnalysisRunner + StaticFindingsRecordedEvent + reviewer-context filter already existed; what was missing was a stage that runs the tool and emits the event. Added: - StaticCheckStageExecutor: reads stage metadata (static_tool/static_argv), runs the configured command via StaticAnalysisRunner, returns a StaticFindingsRecordedEvent. No-op (empty findings) when no runner is wired or no command is configured — safe to carry unconfigured. - A deterministic-stage seam in DefaultSessionOrchestrator.enterStage: any stage with metadata["stage_type"] == "static_check" is run by the executor instead of the LLM subagent, then advances on its (unconditional) exit edge. - TomlWorkflowLoader: stage_type/static_tool/static_argv fields → StageConfig.metadata. - role_pipeline.toml: a static_check stage between implementer and reviewer (no-op until static_argv + a CommandRunner are set; activation is live-QA-gated, see StaticAnalysisRunner doc). §B-§6 — critique-outcome producer. The CritiqueFinding type + CriticCalibrationProjection existed but nothing fed them. Added: - CritiqueFindingsRecordedEvent (+ CritiqueVerdict): the producing side — a critic's findings + verdict for one review iteration, carrying modelHash for per-model calibration. - CritiqueOutcomeCorrelator: pure loop-resolution logic deciding UPHELD (fixed between rounds) / DISMISSED (persisted into an approved final) / INCONCLUSIVE (open at a non-approved terminal), per critic (role + modelHash) and per finding id. - A hook in completeWorkflow/failWorkflow that correlates recorded findings into CritiqueOutcomeCorrelatedEvents at loop resolution — no-op when none recorded, idempotent. (LLM-side finding emission stays a separate model-gated activation.) Tests: StaticCheckStageExecutorTest (4), StaticCheckStageTest integration (1, fake runner → event + transition), CritiqueOutcomeCorrelatorTest (8), CritiqueCalibrationWiringTest integration (1, seeded findings → outcomes at completion), updated RolePipelineWorkflowTest. Full suites for core:events/kernel/critique, infrastructure:workflow, testing:integration green; detekt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-7
@@ -47,6 +47,12 @@ 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,
|
||||
// Deterministic stage marker (§B-§5): `stage_type = "static_check"` runs a static-analysis
|
||||
// tool instead of an LLM subagent; `static_tool`/`static_argv` configure it (see
|
||||
// StaticCheckStageExecutor). Absent for ordinary LLM stages.
|
||||
@param:JsonProperty("stage_type") val stageType: String? = null,
|
||||
@param:JsonProperty("static_tool") val staticTool: String? = null,
|
||||
@param:JsonProperty("static_argv") val staticArgv: String? = null,
|
||||
)
|
||||
|
||||
// condition fields flattened into the transition row
|
||||
@@ -93,6 +99,19 @@ class TomlWorkflowLoader(
|
||||
return if (candidate != null && candidate.exists()) candidate.toString() else raw
|
||||
}
|
||||
|
||||
// Flattens a stage's TOML flags into the StageConfig.metadata string map the runtime reads
|
||||
// (prompt paths, approval/inject/ground flags, and the §B-§5 deterministic-stage markers).
|
||||
private fun stageMetadata(s: StageSection, workflowDir: Path?): Map<String, String> = buildMap {
|
||||
s.prompt?.let { put("prompt", resolvePromptPath(it, workflowDir)) }
|
||||
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")
|
||||
s.stageType?.let { put("stage_type", it) }
|
||||
s.staticTool?.let { put("static_tool", it) }
|
||||
s.staticArgv?.let { put("static_argv", it) }
|
||||
}
|
||||
|
||||
private fun WorkflowFile.toWorkflowGraph(workflowDir: Path?): WorkflowGraph {
|
||||
val startId = StageId(start)
|
||||
val stageMap = stages.associate { s ->
|
||||
@@ -115,13 +134,7 @@ class TomlWorkflowLoader(
|
||||
maxTokens = s.tokenBudget,
|
||||
),
|
||||
maxRetries = s.maxRetries,
|
||||
metadata = buildMap {
|
||||
s.prompt?.let { put("prompt", resolvePromptPath(it, workflowDir)) }
|
||||
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")
|
||||
},
|
||||
metadata = stageMetadata(s, workflowDir),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -53,10 +53,21 @@ class RolePipelineWorkflowTest {
|
||||
val graph = TomlWorkflowLoader(registry).load(path!!)
|
||||
|
||||
assertEquals(
|
||||
setOf("analyst", "architect", "planner", "implementer", "reviewer"),
|
||||
setOf("analyst", "architect", "planner", "implementer", "static_check", "reviewer"),
|
||||
graph.stages.keys.map { it.value }.toSet(),
|
||||
)
|
||||
assertEquals(6, graph.transitions.size)
|
||||
assertEquals(7, graph.transitions.size)
|
||||
|
||||
// The deterministic static_check stage sits between implementer and reviewer.
|
||||
assertEquals("static_check", graph.stages[StageId("static_check")]!!.metadata["stage_type"])
|
||||
assertTrue(
|
||||
graph.transitions.any { it.from == StageId("implementer") && it.to == StageId("static_check") },
|
||||
"implementer should hand off to static_check",
|
||||
)
|
||||
assertTrue(
|
||||
graph.transitions.any { it.from == StageId("static_check") && it.to == StageId("reviewer") },
|
||||
"static_check should hand off to reviewer",
|
||||
)
|
||||
|
||||
// Forward chaining via needs.
|
||||
assertTrue(graph.stages[StageId("reviewer")]!!.needs.map { it.value }.containsAll(listOf("patch", "impl_plan")))
|
||||
|
||||
Reference in New Issue
Block a user