feat(kernel): static-first reviewer gate (role-reliability §5)
Run operator-configured static-analysis commands (compiler/detekt/formatters) as a
deterministic harness step on the producing stage, before the LLM reviewer. A stage
declares `static_analysis = [commands]`; after it produces its artifact, each command
runs in the workspace root and the results are recorded in a StaticAnalysisCompletedEvent
(invariant #9 env observation — recorded live, folded on replay, never re-run). A
non-clean command fails the stage retryably with its output fed back verbatim (§2:
compiler/test output is ground truth), so the implementer fixes it and only static-clean
code ever reaches the reviewer. This makes §5's "reviewer context excludes static findings"
mechanical — the findings are resolved upstream, so the reviewer can't waste inference on
what detekt catches for free; no output-parsing, no context plumbing.
- StaticAnalysisCompletedEvent + StaticAnalysisFinding (core:events) + registration.
- StaticAnalysisRunner seam + ProcessStaticAnalysisRunner (whitespace-split argv, stderr
merged, timeout→nonzero exit) in core:kernel; wired (nullable) into OrchestratorEngines
and the server. Null runner / no workspace root → logged no-op, never blocks the run.
- StageConfig.staticAnalysis + `static_analysis` TOML field; runStaticAnalysis folded into
a runPostStageGates sequence (produces → grounding → echo → static analysis).
- Documented `static_analysis` on the role_pipeline implementer stage (commented; commands
are workspace-specific). The reviewer prompt half shipped in 3467826.
This commit is contained in:
+2
@@ -42,6 +42,7 @@ private data class StageSection(
|
||||
val needs: List<String> = emptyList(),
|
||||
@param:JsonProperty("allowed_tools") val allowedTools: List<String> = emptyList(),
|
||||
val writes: List<String> = emptyList(),
|
||||
@param:JsonProperty("static_analysis") val staticAnalysis: List<String> = emptyList(),
|
||||
@param:JsonProperty("token_budget") val tokenBudget: Int = 4096,
|
||||
@param:JsonProperty("max_retries") val maxRetries: Int = 3,
|
||||
@param:JsonProperty("requires_approval") val requiresApproval: Boolean = false,
|
||||
@@ -106,6 +107,7 @@ class TomlWorkflowLoader(
|
||||
needs = s.needs.map { ArtifactId(it) }.toSet(),
|
||||
allowedTools = s.allowedTools.toSet(),
|
||||
writeManifest = s.writes,
|
||||
staticAnalysis = s.staticAnalysis,
|
||||
tokenBudget = s.tokenBudget,
|
||||
// Propagate the declared token budget to the inference completion cap.
|
||||
// Without this the StageConfig default (maxTokens=2048) is used, truncating
|
||||
|
||||
+19
@@ -156,6 +156,25 @@ class TomlWorkflowLoaderTest {
|
||||
assertEquals(emptyList(), collectStage.writeManifest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `static_analysis maps to the stage static-analysis commands`() {
|
||||
val toml = validToml.replace(
|
||||
"allowed_tools = [\"ShellTool\"]",
|
||||
"allowed_tools = [\"ShellTool\"]\nstatic_analysis = [\"./gradlew compileKotlin\", \"./gradlew detekt\"]",
|
||||
)
|
||||
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(toml) }
|
||||
val graph = loader.load(path)
|
||||
val collectStage = graph.stages[graph.start]!!
|
||||
assertEquals(listOf("./gradlew compileKotlin", "./gradlew detekt"), collectStage.staticAnalysis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `static_analysis absent means no commands`() {
|
||||
val path = Files.createTempFile("workflow", ".toml").also { it.writeText(validToml) }
|
||||
val graph = loader.load(path)
|
||||
assertEquals(emptyList(), graph.stages[graph.start]!!.staticAnalysis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ground_references true maps to metadata key`() {
|
||||
val toml = validToml.replace(
|
||||
|
||||
Reference in New Issue
Block a user