Initiative Tail¶
The two stages between "every plan item is done" and delivery, and the driver that replans an initiative which can no longer advance.
Project Lifecycle owns the graph and the rollup that opens this tail; Plan Review owns everything before dispatch.
Scope
The Build Loop is authoritative on assembly. This page owns the evaluation stage, the stalled-initiative replan and its generation cap, and degraded-boot parking.
INTEGRATING is what the loop does today, and the rest of this page
describes it as built and enforced. The build loop retires it: assembly
stops being a stage, a unit merges onto the trunk as it finishes, the trunk
is green at all times, and the software boots at the end of every slice.
What this stage exists to catch, a set of individually-verified parts that
nobody ever ran together, is answered continuously by the trunk gates
instead of once at the end. Until the trunk gates exist, INTEGRATING is
the only thing catching it, so the state diagram and ## INTEGRATE below
remain the contract rather than a superseded one.
EVALUATING survives unchanged. Scoring the delivered whole against the
charter's criteria is a judgement no gate makes, and it stays fail-closed.
The problem¶
The general loop ran plan, execute, and verify, then stopped. Every plan item passing its own review gate completed the plan, the project, and the objective.
That is a real assurance about each piece and none at all about the whole. An initiative could deliver a set of individually-verified parts that nobody had ever assembled, run together, or checked against what the objective actually asked for, and the board would show it as delivered. Three smaller leaks kept that from even being reliable per piece:
- a WORK plan item could declare zero expected artifacts, which disarmed the fail-loud zero-artifact guard, so a chat-only run with no output reached review as though it had produced something;
- the lifecycle-only baseline execution service walked any task straight to
COMPLETED, gate or no gate; - the coordination parent rollup derived subtask status from run outcomes, which report success before verification.
Shape¶
stateDiagram-v2
EXECUTING --> INTEGRATING: every item done
INTEGRATING --> EVALUATING: assembly job passed its review gate
EVALUATING --> COMPLETED: every success criterion met
INTEGRATING --> EXECUTING: an item regressed
EVALUATING --> EXECUTING: an item regressed
INTEGRATING --> SUPERSEDED: replan
EVALUATING --> SUPERSEDED: replan
INTEGRATING --> FAILED: assembly will not assemble
EVALUATING --> FAILED: the judgement cannot run
EXECUTING -> COMPLETED does not exist. Neither does the project's
ACTIVE -> COMPLETED. Delivery has exactly one predecessor in both machines, so
the tail cannot be skipped by construction rather than by whichever service
happens to be wired. The back-edges carry a regression: an item that stops being
done (integration findings routed back as rework) reopens the build without a
replan.
Both tail stages reach FAILED. A replan resolves the tail failures
somebody chooses to re-plan; without the direct edge, the ones nobody re-plans
sit in the tail with no reachable exit, which is the deadlock shape that made a
whole project undeletable. See
Every state has an exit a writer can always take.
INTEGRATE¶
engine/initiative/integrate.py mints one ordinary task and dispatches it
through the normal work pipeline.
This stage is the root assembly, and on a plan that recursed it is no
longer the only one. An item with children is the assembly of the work below
it, dispatched as an ordinary plan item with an assembly brief over its own
children and its own namespaced evidence paths, keyed on its whole address in
the tree (.synthorg/integration/<slug>/<slug>/, one segment per level) so no
two containers can write over each other, so a wide fan-in at the top becomes
the narrow ones the recursion-depth sweep measured. Those carry their container's
plan_item_id like any other item, so item_is_done reads them normally and
derive_plan_status opens INTEGRATING only once every subtree has assembled.
The root's brief then names the plan's workstreams rather than every leaf in
the tree: listing a hundred titles is what a flat render becomes the moment a
plan is a tree. engine/assembly.py owns the brief, the paths, and the stakes
ladder that both callers share. See
recursive-decomposition.md.
Making it an ordinary task is the whole design. It inherits the entire existing
verification chain with no second oracle written for it: the review gate runs
run_completion_gates, so the build/test oracle reads its CodeExecutionRecord
rows and refuses an unverified or failing build, the completion-oracle peer
review fails closed without a distinct reviewer, and output policy, red team,
and vision all apply. A bespoke "integration checker" would have been a second,
weaker gate.
Three shape decisions carry weight:
| decision | why |
|---|---|
forced LEAF (WorkItem.leaf_required) |
splitting an assembly job hands the pieces back to separate agents, which is the state the stage exists to end |
plan_id set, plan_item_id unset |
it belongs to the initiative without implementing any plan item, so every derivation over items ignores it and it cannot distort the rollup that opened the stage. That provenance rule still identifies exactly the ROOT assembly: a subtree assembly carries its container's plan_item_id and is that item's progress |
id derived from the plan id (uuid5) |
idempotency with no "already started" flag to drift from reality: a re-fired stage finds the existing row and stops |
It declares two expected artifacts, .synthorg/integration/report.md (what was
assembled, where the runnable deliverable is, what had to be fixed) and
.synthorg/integration/end-to-end.txt (the run's own output, verbatim). They
are workspace-relative paths rather than prose because the declared-artifact
check can only probe a path (see
agent-execution.md), and a stage
whose declarations no probe can reach arms nothing: a chat-only integration would
reach review with the check silently abstaining. The stage cannot know where a
given objective's deliverable lives, so it does not guess at that path; it names
two files of its own that the brief instructs the agent to write, and checks
those. The zero-tool-call proxy still applies, and the assembly is judged by the
same review chain as any other task. It runs one stakes level above the plan's
highest item, because assembly is the first point the whole thing runs and the
last point before delivery. Its acceptance criteria are the objective's own.
Outcome, read from the task's persisted status on the next rollup recompute:
COMPLETED(so the gate passed it): planINTEGRATING -> EVALUATING;FAILED/REJECTED/CANCELLED: the replan trigger fires withINTEGRATION_FAILED;- anything else: still working, nothing happens.
EVALUATE¶
engine/initiative/evaluate.py runs a bounded session in which the accountable
lead judges the delivered whole against the objective's success criteria. It is
modelled on the SHIP retrospective session: detached, wall-clock bounded, turn
and cost capped, never raising into the rollup.
The verdict is per-criterion and evidenced (EvaluationReport), not a single
thumbs-up. Two invariants make it load-bearing:
- every criterion is judged exactly once. A submission that drops one, or invents one the objective does not have, is rejected back into the session for correction rather than accepted with the unanswered criteria treated as met;
PARTIALis not a pass. An initiative is delivered when the objective is met, not mostly met.
The session's tools are read-only by design (workspace read and list). A session
that could change what it is judging could turn its own failing verdict into a
passing one. They are scoped to the plan's own project workspace
(engine/workspace/paths.py::project_workspace_dir), not the shared
agent-workspaces base root, so listing a directory returns the deliverable
rather than a tree of sibling projects and the paths in the material resolve
as written.
What the judge is given¶
The material (engine/initiative/evaluate_brief.py) carries the objective and
its criteria, the plan items and their declared artifacts, and the recorded
test-run evidence for the project: the CodeExecutionRecord rows written from
the commands that actually ran. The list is newest-first and bounded. "The test suite
passes" is then judged against what ran rather than against a claim, and the
brief no longer directs the session to run things it has no tool to run.
The verdict is a record¶
The verdict decides whether an initiative delivered, so it is persisted before
anything acts on it. initiative_evaluation_report is an append-only,
dual-backend table keyed unique on (plan_id, attempt), carrying the summary
and every CriterionVerdict with its outcome and evidence
(persistence/evaluation_report_protocol.py, composing AppendOnlyRepository).
A re-evaluation is a new attempt with its own row rather than an edit of the
old one: overwriting would erase the evidence the replan points at.
evaluate.py::_record writes it before the status write, so a lost CAS
race on the completion transition costs the transition rather than a judgement
that cost real money and cannot be re-derived. A record write that fails
parks the plan: it returns False and _run never reaches _apply. A
verdict nobody can read afterwards is, to every later reader, no verdict, and
no verdict parks rather than completes; completing on one would leave an
initiative marked delivered with nothing to point at when asked why. The next
recompute re-judges within the attempt cap, so the cost is a re-judgement
rather than a delivery with no evidence behind it.
GET /plans/{plan_id}/evaluation returns the attempts newest-first, and the
dashboard's PlanEvaluationPanel renders each criterion with the judge's
evidence, so a parked initiative explains itself instead of leaving the
operator with unmet_count=2 in a log line and nothing else. Empty attempts
is the honest answer for a plan nothing has judged; the plan's own status
distinguishes that from one parked at EVALUATING because no verdict landed.
Fail closed¶
No report, an unresolvable lead, no provider, a timeout, an objective with no
criteria: every one of those leaves the plan sitting at EVALUATING with a
warning on each recompute. Nothing completes an initiative on a missing verdict.
This is the deliberate inverse of the red-team gate's policy, and it is the point of the whole change: an evaluation that did not happen is not a pass. An operator resolves a parked plan by replanning or cancelling.
Auto-replan¶
replan_initiative was fully built and reachable only from a human
POST /plans/{id}/replan. Nothing noticed when an initiative ran out of ways to
advance, so a stalled plan simply hung until someone looked.
engine/initiative/replan_trigger.py is that missing driver. The successor
lands in PENDING_REVIEW, which is the human gate the product already has: the
organisation gets itself unstuck, the operator still decides.
What counts as stalled¶
A shape, not a duration: the plan has outstanding work and none of it can move without a new decision. There is no threshold to tune and no timer, and the derivation is exact the moment the last live item dies.
Several cases are deliberately excluded, each because replanning would destroy something:
| not a stall | why |
|---|---|
work still moving (CREATED through IN_REVIEW) |
it is simply in flight |
a human wait (AWAITING_INPUT, AUTH_REQUIRED) |
the org is waiting on the operator; a replan would discard the question rather than answer it |
| a WORK item whose task row does not exist yet | dispatch writes the plan's EXECUTING status before it creates the task rows, so treating this as dead would replan every initiative during its own dispatch window |
| an undecided DECISION item that carries options | a human wait like the row above: somebody can still answer it, and the parked question is how they are asked |
a BLOCKED task parked on a reason someone will still end (oracle_escalated, reviewer_unstaffed, red_team_unstaffed, no_capable_agent) |
the same shape of wait as the two rows above, expressed through BLOCKED instead of its own status. BLOCKED is otherwise dead by default, so this one reads the REASON rather than the status (ATTENDED_BLOCKED_REASONS): without it, asking a human to decide a review is itself what makes the initiative look stalled, and the replan supersedes the plan the question was about |
The two tail stages produce verdicts no derivation over items can see (every
item is COMPLETED when integration fails), so INTEGRATION_FAILED and
EVALUATION_UNMET are carried in by the stage and re-confirmed by the plan
still sitting in the stage that produced them.
A DECISION item has no task row by construction, so the missing-row carve-out
above is narrowed to WORK items, which is the only case its justification (the
dispatch window) describes. Read the other way it made every undecided decision
count as live for ever: a plan whose one outstanding item was a decision could
never derive a stall, so the replan trigger never fired and the initiative hung
with nothing watching. An undecided DECISION with no options counts as
dead: nobody can resolve it, so the plan derives BLOCKED and replans.
plan_validation.validate_decision_options additionally rejects an optionless
DECISION at parse time, so the unanswerable shape stops being producible.
Loop safety¶
Plan.replan_generation counts how many times an initiative has replanned
itself. A successor opened automatically carries its predecessor's generation
plus one; a human replan resets it to zero, because a human decision is not a
runaway. Past engine.auto_replan_max_generations the trigger refuses.
Every fire re-reads the plan and re-confirms the stall before doing anything, which is what makes a redelivered rollup event harmless: the first replan supersedes the plan, and every later attempt reads a superseded plan and stops.
A refusal is an answer¶
The trigger owns two refusals nobody else can see: the generation cap and the
engine.auto_replan_enabled master switch. Both used to be evaluated inside
the detached task, where the rollup could not learn of either, so it read "a
trigger is attached" as "a replan will happen" and asked again on every
recompute. A live run scheduled a replan that was refused three times in twelve
minutes and kept going, while its plan read executing with four of seven
items dead and a warning rewritten in the log as the only trace.
So the rollup asks, and the trigger answers with a
ReplanDisposition. SCHEDULED, ALREADY_RUNNING and UNAVAILABLE all mean
something is or will be happening; DISABLED and BUDGET_EXHAUSTED mean no
automatic route remains, and so does no trigger being attached at all. Those
three are one outcome with three reasons.
No automatic route left¶
An initiative in that state needs a person, and the loop's own rule for
reaching one is by exception: a question needing an answer. So
engine/initiative/stall_escalation.py keeps exactly one OPEN
initiative:stalled decision per plan, sends one notification on the edge that
opens it, and leaves the plan where it is, still open to a manual replan while
the operator thinks. Later passes find the decision open and say nothing:
neither a second alert, nor a second ask of the replan trigger, since an
initiative waiting on a person is not one to keep considering for an automatic
replan, and refusing it at WARNING every cadence would be the repeating log
line the decision replaced.
One OPEN rather than one ever. Answering the decision closes it, and a plan that is still stalled on the next pass raises a fresh one. That is the correct reading: the answer was taken, it did not move the initiative, and the operator is owed the news rather than silence. What must hold for it to stay bounded is that answering CHANGES something, which is why the answer is re-confirmed against the live state on the branch the recorded reason selects rather than re-derived over the items alone.
Failing the plan there was the tempting answer and is the wrong one twice over.
It is the system deciding whether an initiative the operator may still want
should end, which is a de-escalation of a decision the human owns; and it does
not even fix the surface it looks like it fixes, because the objective task is
held at IN_PROGRESS until the plan COMPLETES, so the initiative's own board
row would not move either. What moves instead is the plan's own surface: the
open decision is resolved beside the plan row (api/_plan_decisions.py) and
rendered as "Awaiting your decision", so executing stops being the whole
story.
Both answers act. Approving replans the initiative once on the operator's
authority through ReplanTriggerService.grant, which applies neither the cap
nor the switch (both bound what the org does unasked, and somebody has just
asked) and stamps generation zero on the successor. Rejecting fails the plan
with the stall reason. A plan that recovered in between is re-confirmed and
left alone either way.
Degraded boots¶
Each collaborator degrades independently, and none of them degrades into a completion:
| unwired | behaviour |
|---|---|
| integrate stage (no work pipeline) | plan parks at INTEGRATING, WARNING per recompute |
| evaluate stage (no provider) | plan parks at EVALUATING, WARNING per recompute |
| replan trigger (no coordinator) | a derived stall escalates to the operator as a decision, exactly as an exhausted budget does |
| stall escalation (no approval store) | a derived stall fails the plan with its reason; parking it silently is what left a dead initiative reading EXECUTING |
| extension escalation (no approval store) | a workstream needing another extension is not held at EXECUTING; the plan advances with the gap unaddressed, surfaced later at the judged EVALUATING gate rather than by a second parking mechanism |
| retro capture (no memory layer) | finished work does not feed a retrospective back |
Parking is the honest outcome while something can still move it: an initiative whose pieces were never assembled has not delivered, and an initiative nobody scored has not been shown to meet its objective. The operator's remedy is one the product already has: replan (legal from both tail stages) or cancel.
Parking a plan nothing can move is a different thing, and it is a deadlock. A
derived stall that no automatic route can clear has no remaining actor of its
own: the items are all dead, the stages cannot fire, and no later event changes
either fact. The remaining actor is a person, so it escalates; only where
nothing in the deployment can ask one does the rollup fail the plan with the
stall reason rather than parking it. An unsuccessful coordinate(...) fails
the plan exactly as a raised one does. Both were the same collapse in a live
run: five tasks died in 1.85s and the plan sat at EXECUTING for ever, because
only a raise had been treated as failure.
Independently means one subsystem each, six of them, all separate from the
rollup. The rollup activates once persistence and the task engine exist,
which is before setup has configured a provider, so a first boot legitimately
produces a rollup with no tail; each initiative_* spec waits on what that one
collaborator actually needs and activates on a later reconciler pass, attaching
onto the already-wired rollup without re-registering the observer, so each comes
online with no restart.
Declaring the six as one subsystem would make the union of their requirements a precondition for any of them, and the table above would be a lie: a boot with no coordinator would get no integrate stage either. The stall escalation is its own subsystem for the sharper version of that reason: it is needed exactly when the replan trigger is absent or refusing, so folding it into the trigger's spec would leave a boot with one reading as covered for the other. Extension escalation is separate again, on the same reasoning: it raises a different decision under a different idempotency key (see Workstream Extension), and a plan whose stall escalation is wired but whose extension escalation is not should still read that gap rather than borrow the sibling's liveness. Their liveness is read one probe per collaborator for the same reason: a shared probe let a tail whose retro capture never resolved (memory blocked because no embedder was chosen) read as converged, and the reconciler never revisits that.
The retro capture additionally declares a teardown and rebuild_on_change,
because it holds both memory backends for the life of the instance and those
are replaceable while the process runs; without them it would keep writing into
layers nothing else reads.
The evaluate stage therefore reads the replan trigger per verdict rather
than capturing one at construction. The two converge on their own schedules, so
a coordinator arriving after the provider registry would otherwise leave the
stage holding the None it was built with and park every unmet initiative for
the life of the process.
There is exactly one wiring path per collaborator. Re-running the rollup's own wiring attaches nothing: a post-setup rewire list is the drift subsystem reconciliation exists to reject, and it is what left the tail declining on every deployment while the capability read as live.
Settings¶
Every key below is read when the tail fires, so an edit applies to the next run of it rather than needing a restart.
| key | default | purpose |
|---|---|---|
engine.auto_replan_enabled |
true | master switch for the stall trigger |
engine.auto_replan_max_generations |
2 | generation cap stopping a runaway chain |
engine.auto_replan_timeout_seconds |
600 | ceiling on one re-decomposition |
engine.integration_stage_timeout_seconds |
1800 | ceiling on minting + dispatching the assembly job |
engine.evaluation_session_max_turns |
10 | evaluate session turn cap |
engine.evaluation_session_cost_ceiling |
1.0 | evaluate session spend ceiling |
engine.evaluation_session_timeout_seconds |
300 | evaluate wall-clock ceiling |
coordination.jit_extension_planning_enabled |
false | master switch for the extension mechanism; see Workstream Extension |
engine.auto_extension_enabled |
true | automatic-authority switch for granting an extension unasked, once the master switch is on |
engine.auto_extension_max_generations |
2 | generation cap, per workstream, stopping a runaway extension chain |
What proves the tail ran¶
Reading the code cannot settle whether the tail fires, and neither can watching
a run go by: three live runs each ended with someone asserting from inspection
that the machinery was wired, while no plan had ever reached INTEGRATING. So
the claims are stated here as rows a query returns, and a run that cannot
produce those rows has not proved anything regardless of what its log said.
The oracle blocked an unverified build. The blocking authority is the
integration task's ordinary review gate, whose build/test oracle is a pure
function of persisted code_execution_record rows.
| evidence | where it is read back |
|---|---|
the integration task exists, plan_id set and plan_item_id null, and is not completed |
tasks row, GET /tasks/{id} |
| no passing test evidence backs it | code_execution_record rows for the project |
the plan did not advance out of integrating |
GET /plans/{id}, GET /plans/{id}/transitions |
| the stall was named, not swallowed | plans.replan_generation, the initiative.integration.failed event |
| the stall reached a person once it could go no further | the initiative:stalled row in GET /approvals, PlanRow.pending_decision |
| no evaluation report was written | GET /plans/{id}/evaluation is empty |
A passing wave marked the objective complete.
| evidence | where it is read back |
|---|---|
| a report with one verdict per objective criterion, each met, each with evidence | GET /plans/{id}/evaluation |
| the report was written before the status changed | initiative_evaluation_report.created_at against the lifecycle_transitions row |
the evaluating -> completed hop names initiative-evaluate |
GET /plans/{id}/transitions |
| the project mirrored and the objective task closed | projects, tasks |
| the deliverable exists and runs | .synthorg/integration/ in the project workspace |
The transition ledger carries the actor for every hop, so "who moved this" is a
query rather than an inference. That is load-bearing for the second table: a
completed plan whose transition names anything but initiative-evaluate
reached the status by a path this design forbids, and the row is the only place
that shows it.
The end-to-end run walks a run through the dashboard and reads each row back.
Enforcement¶
scripts/check_verified_completion_paths.py (pre-push + CI) holds up the three
structures this page depends on: the forbidden edges stay forbidden and
COMPLETED keeps exactly one predecessor in both machines; only the evaluate
stage writes PlanStatus.COMPLETED; and both plan-shaped models still enforce
that a WORK unit declares a deliverable. Opt out per-line with
# lint-allow: verified-completion -- <reason>.