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
| Key | Required | Type | Purpose |
|---|---|---|---|
run | yes | string | The run slug, e.g. dupefind. |
branch | yes | string | The run's branch, e.g. run/dupefind. |
phase | yes | enum | 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. |
profile | no | enum | patch | standard | full. Absent → full — the default is heaviest, not lightest, so every pre-profile run record keeps its meaning unchanged. |
paused_reason | when paused | string | 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. |
closure | when closed | object | Required when phase: closed, absent otherwise. See Closure record. |
intake | no | object | Optional staging provenance — written once at staging time and never edited after. Contains source, ref, url, client_key, staged_by. |
budget | no | object | 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). |
gates | yes | map | 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. |
tasks | no | list | Task bookkeeping — the only home of review_rounds. Each entry has id, status, and review_rounds. |
escalations | no | list | 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.
| Profile | Gates | Phases |
|---|---|---|
patch | G1, G2 | plan → implement → integrate → done |
standard | G0, G1, G2 | spec → plan → implement → integrate → done |
full | G0, G1, G2, G3 | spec → 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
| Key | Type | Purpose |
|---|---|---|
approved | boolean | Whether the gate has been approved |
by | string | null | The named human who decided |
at | string | null | ISO-8601 timestamp of the decision |
notes | string | null | Approval or decline notes |
burden | enum | null | How 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.
| Key | Type | Purpose |
|---|---|---|
as | enum | The typed disposition: already-delivered | superseded | obsolete | abandoned |
by | string | The named human; never a bot identity |
at | string | ISO-8601 timestamp |
reason | string | Required — the comment on the disposition |
Budget ledger
The budget.ledger[] is append-only, one entry per model
invocation. Each entry carries:
| Key | Type | Purpose |
|---|---|---|
at | string | ISO-8601 timestamp |
role | string | Role name |
task | string | null | Task id, null when not applicable |
round | number | null | Review round, null when not applicable |
adapter | string | Adapter name |
model | string | Concrete model ID used |
tokens_in | number | null | Input tokens consumed. Null in v0 records where the harness reports combined tokens only. |
tokens_out | number | null | Output tokens consumed. Null when not split by the harness. |
cost_usd | number | null | Cost in USD. An entry with cost_usd: null and no failed flag is an open entry: an agent in flight. |
failed | boolean | absent | Optional 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. |
refused | boolean | absent | Optional: 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. |
engine | string | absent | Optional: 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. |
session | string | absent | Optional: 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):
- CAS ref updates. Every write is a compare-and-swap:
the writer reads the tip, produces a comment-preserving mutation,
and calls
git update-refwith 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. - Comment-preserving YAML. Edits use document-level APIs that preserve comments and formatting; the writer never re-serializes from scratch.
- ISO-8601 timestamps. All
atfields are machine-written in ISO-8601; a bare date parses but decision-latency metrics degrade. - 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: …], andreopened to <phase> by <name> (was closed as …). The orchestrator uses bookkeeping verbs under a dedicated bot identity:dispatched | bounced | advanced | escalated | paused | metered | harvested ….pausedis shared:gateline pausewritespaused by <name> (<reason>)under the human's identity, and only the author distinguishes the two. - review_rounds in state.yaml only. No other file carries it.
- Phase/status values within contract enums. No ad-hoc values.
- 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.