Implement closed-loop workspace follow-ups

This commit is contained in:
2026-07-15 23:48:12 +04:00
parent 3a48ecd24f
commit ed7efb6072
51 changed files with 702 additions and 54 deletions
+1
View File
@@ -15,6 +15,7 @@ Adapter for `core:transitions` and `core:inference` workflow interfaces. Depends
- `ExecutionPlanCompiler` converts the parsed `ExecutionPlanModel` into a runnable plan for `core:transitions`; compilation is deterministic given the same input model.
- `PlanLinter` enforces structural rules (e.g. unreachable stages are rejected); linting failures throw `WorkflowValidationException`.
- `PlanDerivedManifest` exposes the write manifest derived from a plan.
- For a plan with no explicit build expectation, `ExecutionPlanCompiler` marks every write-declaring stage for the runtime auto build gate; plans with no declared writes are not represented as compiler-verified.
## Work Guidance
@@ -55,7 +55,7 @@ private const val RECOVERY_PROMPT =
// round, evicting the file_read results the model just gathered, so it re-reads forever and never
// accumulates enough to write. Match the static execution baseline. (gemma ctx is 32768, so this
// leaves ample headroom for completion.)
private const val DEFAULT_STAGE_TOKEN_BUDGET = 16384
private const val DEFAULT_STAGE_TOKEN_BUDGET = 24576
// A compiled freestyle stage must also lift its inference completion cap off StageConfig's 2048
// default, or the model is truncated (finishReason=length) mid-artifact — and a degenerating local
@@ -114,8 +114,11 @@ class ExecutionPlanCompiler(
// ACTUALLY builds is decided at run time (SessionOrchestrator.runExecutionGate) from the real
// FileWritten manifest — the plan's declared kinds don't reveal code-ness, but the written
// paths do — so a docs-only plan is left alone and a code plan is always gated.
val autoGateStageId: String? =
if (declaredExpectations.values.all { it == BuildExpectation.NONE }) terminalStageId(plan) else null
val autoGateStages: Set<String> = if (declaredExpectations.values.all { it == BuildExpectation.NONE }) {
plan.stages.filter { it.writes.any { path -> path.isNotBlank() } }.map { it.id }.toSet()
} else {
emptySet()
}
val stageMap = plan.stages.associate { s ->
// `produces` is the unique slot name referenced by needs/edges; `kind` selects the
@@ -149,7 +152,7 @@ class ExecutionPlanCompiler(
expectedFiles = s.writes.map { it.trim() }.filter { it.isNotEmpty() && !isGlob(it) },
touches = s.touches.map { it.trim() }.filter { it.isNotEmpty() },
buildExpectation = declaredExpectations.getValue(s.id),
autoBuildGate = s.id == autoGateStageId,
autoBuildGate = s.id in autoGateStages,
semanticReview = s.semanticReview,
tokenBudget = DEFAULT_STAGE_TOKEN_BUDGET,
generationConfig = defaultStageGeneration,
@@ -470,9 +470,9 @@ class ExecutionPlanCompilerTest {
"goal": "scaffold a react app",
"stages": [
{ "id": "entry", "prompt": "write main.tsx", "produces": "app_entry", "kind": "react_entry",
"needs": [], "tools": ["file_write"] },
"needs": [], "tools": ["file_write"], "writes": ["src/main.tsx"] },
{ "id": "views", "prompt": "write components", "produces": "app_views", "kind": "react_component",
"needs": ["app_entry"], "tools": ["file_write"] }
"needs": ["app_entry"], "tools": ["file_write"], "writes": ["src/App.tsx"] }
],
"edges": [
{ "from": "entry", "to": "views", "condition": { "type": "always_true" } },
@@ -482,12 +482,12 @@ class ExecutionPlanCompilerTest {
""".trimIndent()
@Test
fun `terminal stage of an ungated plan is flagged for an auto build gate`() {
fun `every write-declaring stage of an ungated plan is flagged for an auto build gate`() {
val graph = codeCompiler.compile(codePlanNoGate, "wf")
val views = graph.stages[StageId("views")]!!
assertTrue(
views.autoBuildGate,
"terminal stage of an ungated plan must be flagged for an auto build gate",
"a write-declaring stage of an ungated plan must be flagged for an auto build gate",
)
assertEquals(
com.correx.core.transitions.graph.BuildExpectation.NONE,
@@ -496,8 +496,8 @@ class ExecutionPlanCompilerTest {
"code module was actually written",
)
assertTrue(
!graph.stages[StageId("entry")]!!.autoBuildGate,
"intermediate stages are not flagged — the gate runs when the project is whole, not per-stage",
graph.stages[StageId("entry")]!!.autoBuildGate,
"implementation stages must not defer compiler coverage to a terminal verifier",
)
}
@@ -531,15 +531,15 @@ class ExecutionPlanCompilerTest {
}
@Test
fun `exactly the terminal stage of an ungated plan is auto-flagged`() {
// The compiler flags the terminal stage regardless of the produced kinds — code-ness is a
// run-time decision (SessionOrchestrator reads the FileWritten manifest), so a non-code plan
// is flagged too but the runtime build gate then skips it.
fun `plan without declared writes has no auto build gate`() {
// A plan with no write declarations cannot promise compiler coverage. Runtime still
// inspects actual files for an opted-in gate, but the compiler must not pretend this
// documentation-shaped plan has implementation-stage verification.
val graph = compiler.compile(validPlan, "wf")
assertEquals(
1,
0,
graph.stages.values.count { it.autoBuildGate },
"exactly the single terminal stage of an ungated plan is flagged for the auto build gate",
"only write-declaring stages receive an auto build gate",
)
}
}