feat(validation): plan-compile gate + semantic-review gate (staged verification #19/#20)

Plan-compile gate (#19): PlanCompilationCheck seam (null-on-replay),
runPlanCompileGate guards a produced execution_plan slot and compiles its
cached content via ExecutionPlanCompiler, emitting PlanCompileCheckedEvent.
On failure the architect gets a retryable hand-back and self-corrects;
exhaustion terminates the dead-end workflow instead of shipping an
uncompilable plan.

Semantic-review gate (#20): SemanticReviewer seam + StageConfig.semanticReview
opt-in (TOML semantic_review + freestyle plan JSON). runReviewGate reads the
FileWritten manifest (never blind context), asks a review-capable model for
PR-comment findings, and is advisory — blocks retryably only on FAIL plus a
correctness finding >=0.7 confidence, capped at 2 blocks so a stubborn stage
completes advisory-only rather than trapping. SemanticReviewerImpl talks to
the provider directly; any reviewer error degrades to PASS. REVIEW_MAX_TOKENS
is 8192: gemma is a reasoning model and 1024 truncated it mid-reasoning before
it reached the JSON verdict (empty content -> parse degraded to PASS).

Both gates live-QA'd on the real repo: plan-compile passed on a freestyle
web-UI scaffold; semantic review caught all 3 planted bugs in a maxOf() and
exercised the block->retry->cap safety valve end-to-end.
This commit is contained in:
2026-07-06 13:11:16 +04:00
parent 28e9e698a1
commit 9f12c87bb1
15 changed files with 547 additions and 2 deletions
@@ -74,6 +74,7 @@ class ExecutionPlanCompiler(
// not existence guarantees, so they are excluded here.
expectedFiles = s.writes.map { it.trim() }.filter { it.isNotEmpty() && !isGlob(it) },
buildExpectation = buildExpectation,
semanticReview = s.semanticReview,
tokenBudget = DEFAULT_STAGE_TOKEN_BUDGET,
metadata = mapOf("promptInline" to s.prompt),
)
@@ -25,6 +25,9 @@ data class PlanStage(
// planner emits this under the JSON key `build_expectation`; it resolves at runtime to the
// project profile's typecheck/build/test command run as a deterministic Gate 4.
@param:JsonProperty("build_expectation") val buildExpectation: String? = null,
// Opt-in to the Gate 3 semantic (LLM) reviewer for this stage (staged-verification § Gate 3).
// The planner emits this under the JSON key `semantic_review`; absent = off.
@param:JsonProperty("semantic_review") val semanticReview: Boolean = false,
)
data class PlanEdge(
@@ -51,6 +51,7 @@ private data class StageSection(
@param:JsonProperty("brief_echo") val briefEcho: Boolean = false,
@param:JsonProperty("require_task_decompose") val requireTaskDecompose: Boolean = false,
@param:JsonProperty("claim_task") val claimTask: Boolean = false,
@param:JsonProperty("semantic_review") val semanticReview: Boolean = false,
)
// condition fields flattened into the transition row
@@ -110,6 +111,7 @@ class TomlWorkflowLoader(
allowedTools = s.allowedTools.toSet(),
writeManifest = s.writes,
staticAnalysis = s.staticAnalysis,
semanticReview = s.semanticReview,
tokenBudget = s.tokenBudget,
// Propagate the declared token budget to the inference completion cap.
// Without this the StageConfig default (maxTokens=2048) is used, truncating