state.yaml reference

state.yaml lives at runs/<slug>/state.yaml and is the single source of truth for a pipeline run: its phase, profile, budget, gate decisions, task bookkeeping, and escalations. Every consumer (the Gatehouse frontend, the CLI, and the v1 orchestrator) reads this file first to derive what comes next, together with the artifacts and tasks/*.yaml on the run branch (the tasks: block mirrors those files). It is co-written by a named human (gate decisions) and the orchestrator (bookkeeping) under a structured contract; no run state lives outside the run directory.

Top-level keys

KeyRequiredTypePurpose
runyesstring The run slug, e.g. dupefind.
branchyesstring The run's branch, e.g. run/dupefind.
phaseyesenum One of spec | plan | implement | integrate | release | done | paused | closed. Reduced profiles skip phases per their gate set. done and closed are both terminal: done finished its pipeline, closed was ended short of it by a human.
profilenoenum patch | standard | full. Absent → full — the default is heaviest, not lightest, so every pre-profile run record keeps its meaning unchanged.
paused_reasonwhen pausedstring A free string. The conventional values are budget-exhausted | round-cap | escalation | gate-declined | staged (the contract comment's list) plus slug-landed, which the engine sets when runs/<slug>/ has already shipped on the default branch and the contract comment does not list yet. The schema does not enforce the list: gateline approve --hold <reason> writes the operator's free-text reason here. Set when phase: paused; null otherwise.
closurewhen closedobject Required when phase: closed, absent otherwise. See Closure record.
intakenoobject Optional staging provenance — written once at staging time and never edited after. Contains source, ref, url, client_key, staged_by.
budgetnoobject Cost tracking: cost_limit_usd (exhaustion pauses, never silently degrades), cost_spent_usd (derived from ledger), and ledger[] (append-only, one entry per model invocation).
gatesyesmap One entry per gate in the profile's gate set (G0–G3). A gate outside the profile is absent, never auto-approved; a gate inside the profile that is missing makes the file malformed. Written ONLY by named humans.
tasksnolist Task bookkeeping — the only home of review_rounds. Each entry has id, status, and review_rounds.
escalationsnolist Append-only escalation records: at, from_role, reason, resolved, and optional disposition.

Profiles and their gate sets

The three profiles are fixed sets, not knobs. A gate that does not exist for the profile is absent, never auto-approved. An absent gate entry parses as undecided, so it can never count as approved.

ProfileGatesPhases
patchG1, G2plan → implement → integrate → done
standardG0, G1, G2spec → plan → implement → integrate → done
fullG0, G1, G2, G3spec → plan → implement → integrate → release → done

paused and closed are rest states every profile can reach from any phase in its sequence.

Profile upgrades are one-way and human-decided: a human edits profile: to a heavier value and resumes; the reconciler derives the backfill. Downgrading mid-run is forbidden, and an engine that observes a profile lighter than the already-decided gates escalates (rule D21).

Gate entry shape

KeyTypePurpose
approvedbooleanWhether the gate has been approved
bystring | nullThe named human who decided
atstring | nullISO-8601 timestamp of the decision
notesstring | nullApproval or decline notes
burdenenum | nullHow much work the review took: confirmation | light-correction | heavy-correction. The pilot's headline metric for the v1 trust ladder.

Closure record

A run a human ends short of done carries a closure block beside phase: closed. Only the named human who closed the run writes it; gateline reopen clears it in a commit of its own. Closing deletes nothing: the branch, the run directory and every artifact stay put.

KeyTypePurpose
asenumThe typed disposition: already-delivered | superseded | obsolete | abandoned
bystringThe named human; never a bot identity
atstringISO-8601 timestamp
reasonstringRequired — the comment on the disposition

Budget ledger

The budget.ledger[] is append-only, one entry per model invocation. Each entry carries:

KeyTypePurpose
atstringISO-8601 timestamp
rolestringRole name
taskstring | nullTask id, null when not applicable
roundnumber | nullReview round, null when not applicable
adapterstringAdapter name
modelstringConcrete model ID used
tokens_innumber | nullInput tokens consumed. Null in v0 records where the harness reports combined tokens only.
tokens_outnumber | nullOutput tokens consumed. Null when not split by the harness.
cost_usdnumber | nullCost in USD. An entry with cost_usd: null and no failed flag is an open entry: an agent in flight.
failedboolean | absentOptional flag the v1 engine writes when the harness ran and did not complete (crash, timeout, aged out). cost_usd is real usage when known, else the registry estimate.
refusedboolean | absentOptional: no process was spawned (a held checkout, a preflight error). cost_usd is 0, and the entry counts as neither a failure nor a retry.
enginestring | absentOptional: which orchestrator process opened the entry, as <hostname>:<pid>, so the stale sweep can tell a crashed process's orphan from a live one's job.
sessionstring | absentOptional: the runner's own session id; a retry of the same role, task and round resumes it. A hint, never a handle.

The schema's budgetSchema uses .passthrough(); parseLedger in packages/core/src/record/ledger.ts reads the entries defensively.

The entries are facts, not running totals. cost_spent_usd is derived from them, so races and audits survive. In v0 the ledger is hand-appended from harness usage output; in v1 the dispatch seam appends entries automatically.

Task entries

Each task entry mirrors tasks/*.yaml status. Status is one of: pending | dispatched | failed | in-progress | in-review | review-approved | verified | done. review_rounds lives here and here only. The engine reads in-progress as in flight only alongside an open ledger entry for the task, and otherwise returns the task to pending.

Escalation entries

Append-only. Each escalation carries: at, from_role, reason, resolved (boolean), resolved_by, resolved_at, resolution, and disposition, one of re-review | return-to-implement | re-plan (optional machine-actionable route for the engine; absent → engine default).

Co-writer contract

All writes to state.yaml follow a seven-point co-writer contract (ORCHESTRATOR.md §7 introduces it as "six conventions" but numbers seven; the seven are listed here):

  1. CAS ref updates. Every write is a compare-and-swap: the writer reads the tip, produces a comment-preserving mutation, and calls git update-ref with the expected-old OID. A CAS refusal means a concurrent write landed; the engine re-derives from the new tip, and the CLI reports the refusal for the human to rerun.
  2. Comment-preserving YAML. Edits use document-level APIs that preserve comments and formatting; the writer never re-serializes from scratch.
  3. ISO-8601 timestamps. All at fields are machine-written in ISO-8601; a bare date parses but decision-latency metrics degrade.
  4. Structured commit grammar. Human decisions use five reserved forms: state(<slug>): G<N> approved by <name> [burden: …], staged by <name> [client-key: <key>], armed by <name>, closed by <name> [disposition: …], and reopened to <phase> by <name> (was closed as …). The orchestrator uses bookkeeping verbs under a dedicated bot identity: dispatched | bounced | advanced | escalated | paused | metered | harvested …. paused is shared: gateline pause writes paused by <name> (<reason>) under the human's identity, and only the author distinguishes the two.
  5. review_rounds in state.yaml only. No other file carries it.
  6. Phase/status values within contract enums. No ad-hoc values.
  7. Only the gate on the table can be decided. A gate decision is legal only for the profile's first un-approved gate, and only while the run stands in one of the phases that gate is decided in. The CLI, the API and the PR-approval sync all refuse anything else.

Gate authorship — structural, not behavioral

The orchestrator has no code path that writes gates.*.approved or closure. Gate entries and the closure record are reserved for named humans. That is structural, not a behavioral promise. The engine's role is to derive the next action from committed state, not to decide. The role spec itself states this: the orchestrator presents gates but never approves them, and halts until a named human records an approval.