Implement closed-loop workspace follow-ups
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+7
-4
@@ -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,
|
||||
|
||||
+12
-12
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user