How gates keep a human in charge

In an orchestrator-driven run, the engine dispatches the agents, meters the spend, and moves the state file forward. The design's answer to "who is actually in charge?" has four parts: a prohibition built into the code, a shared-file discipline both writers obey, one authority per deployment, and an autonomy level that is earned and measured instead of declared.

A structural "never"

gateline has two operating modes. In v0 a human plays the Orchestrator role, dispatching each agent by hand and moving state.yaml through its phases. In v1 an engine executes the same role spec: the specs are identical in both modes, and only who executes orchestrator.md changes. The engine is a stateless reconciler. It wakes on a trigger, reads state.yaml at the run branch tip, derives the next action from committed files alone, executes it, commits, and exits. No conversation state survives between wakes, so waiting at a gate costs nothing: a gate wait is simply a state from which no action derives.

The load-bearing safety property is structural. The engine has no code path that writes gates.*.approved: "the orchestrator never approves a gate" is not an instruction a model is asked to follow but a property the code cannot violate. The one gate entry a machine writes is the pull-request approval sync, which copies a human reviewer's PR approval into G2 under that reviewer's name; it runs in Gatehouse's server and gateline sync, never in the engine. A run's closure block sits on the same read-only list, so the engine can pause a run and never end one. The role spec says the same in its own grammar: the Orchestrator owns the gates in the sense that it presents them, and it must halt until a named human records approval. And a deterministic loop that meets a state it has no rule for escalates, where a model in the same position would improvise.

One file, two writers

state.yaml is the entire control plane in both directions: there is no orchestrator API, config channel, or command queue. Humans speak through Gatehouse, the framework's web UI, and the gateline CLI; the engine speaks by advancing state that the human surfaces recognize as needing a person. Two writers on one file need rules, and the co-writer contract supplies seven. Every write is a compare-and-swap ref update. YAML edits preserve comments and formatting, through a document API rather than re-serialization. Timestamps are written in ISO-8601 form. Commit messages follow a structured grammar under distinguishable identities. Review-round counts live only in state.yaml. Phase, status, and pause values stay within the contract's enums. And a gate decision is legal only for the gate on the table. That last rule means the profile's first undecided gate, while the run stands in a phase that gate is decided in. Gatehouse, the CLI, the API, and the pull-request approval sync all refuse an out-of-order approval.

Compare-and-swap (CAS) is what makes the two writers safe. Each writer reads the branch tip, makes its edit, commits, and then updates the ref only if it still points at the commit it read. If the ref moved (a human decided mid-tick, an agent committed), the update is refused; the loser discards its work and re-derives from the new tip. A refused write is the designed outcome of a race, not an error, and it is how a human decision always wins: the engine never forces its bookkeeping through.

Provenance is legible at a glance. Engine commits are authored by a dedicated bot identity (gateline-orchestrator, one per install), never a person's git config, and they use a reserved set of bookkeeping verbs: dispatched, bounced, advanced, escalated, paused, metered, harvested. The decision grammar (G2 approved by <name>, staged by <name>, armed by <name>, closed by <name>, reopened to <phase> by <name>) is reserved for humans, and the metrics reader treats it as authoritative. A run's branch history therefore reads like a transcript of the partnership:

c4b9d95 state(creation-seam): G2 approved by Nathan Carter [burden: confirmation]
fdef5f2 state(creation-seam): metered verifier $3.11

The creation-seam record shows both hands in the same file: ledger entries written by the engine, each carrying the dispatch's cost and the token counts the harness reported (the input count leaves out cached context, so the cost column is the one to trust), and gate entries written by the named human, each carrying a burden rating of how much work the review took. Its engine commits carry the bot name of their day, agentic-orchestrator. Finished runs keep the author that wrote them, and the current identity is gateline-orchestrator.

Two writers, one state file. Human and engine both read the branch tip, edit, and commit; the compare-and-swap ref update admits exactly one of them, and the loser re-derives.
Two writers on one state file: human (top lane) and engine (bottom lane), each reads, edits, commits, and CAS-updates the ref. The refused writer re-derives. human — via Gatehouse / gateline CLI read branch tip edit (decision) commit state(<slug>): G2 approved by <name> reserved human grammar engine — tick read branch tip derive next action edit (bookkeeping) commit state(<slug>): metered verifier $3.11 bot identity: gateline-orchestrator refs/heads/ run/<slug> update-ref (CAS) only if ref still = the commit I read ✓ accepted ✕ refused discard; re-read; re-derive — refusal is the designed outcome seven rules: CAS writes · comment-preserving YAML · ISO-8601 timestamps · structured commit grammar · review_rounds only here · contract enums only · gate-on-the-table only

Caps, pauses, and the escalation path

Autonomy inside a phase has hard edges. Implementer ⇄ Reviewer cycles are capped at three rounds per work item; a fourth round escalates automatically, with both sides' artifacts. Resolving that escalation grants one more round, and the engine asks again for each extra round. Spend is capped too: every model invocation flows through the dispatch seam and is metered into the run's budget ledger, and before any dispatch the engine projects the ledger sum plus a static per-role estimate against the run's cost limit. A projected exceedance pauses the run as budget-exhausted and escalates. The engine never degrades quality to fit the budget. Resuming from that pause carries the budget decision: gateline resume --cost-limit <usd> writes a higher limit in the same commit as the phase restore, and a resume without one is refused, because the pause is a condition the engine recomputes on every tick.

Metering is unconditional, but enforcement is a switch. Operators on flat-rate-billed harnesses can start the engine with --no-budget-enforcement; the ledger, derived totals, and token counts record regardless. Pause-don't-degrade governs enforcement, not measurement. The creation-seam run shows both halves in one incident.

With the ledger at $44.02, the engine projected $68.02 against the run's $60 limit, paused, and escalated. The human resolved the escalation by restarting the engine with budget enforcement disabled and resuming; metering kept running, and the record closes at $104.54 spent against the original $60 limit: the machine's stop, the human's override, and the full cost all legible in the same file.

Approve-and-hold. Sometimes signing a gate should not start the next phase: another human decision still stands in between. gateline approve --hold <reason> signs the gate and sets phase: paused in the same commit; the held run is an ordinary rest state, and resume releases it into whatever phase the gate ledger implies. Signing and advancing are separate acts so that a signature can never accidentally launch work.

One authority per deployment

The control plane has one authority per deployment. Gatehouse and the engine run as one supervised unit over one clone (the gateline up topology) with one sync loop and one push path; the git host's origin is the linearization point, so a dispatch whose intent commit origin rejects is never launched. An origin that is merely unreachable does not block the launch, because the clone is only ahead, and a later accepted push carries the commit. A deployment can also run local-only, with no push, no fetch, and no GitHub calls (gateline up --local-only); it is a named, first-class topology. The rule was learned from incidents: the topology design records a day of split deployment in which an engine kept deriving against a stale budget limit, and machine bookkeeping and human decisions accumulated on divergent histories that needed manual repair.

The blessed checkout stays on the default branch, and a code-tree monitor enforces it. Only a clean fast-forward of the default branch counts as an update; on one, the engine drains in-flight work and exits with code 75: a deliberately ordinary "temporary failure" exit that a supervisor restarts onto the new code (unsupervised, the operator restarts it by hand). A dirty tree, a branch switch, or any non-fast-forward movement pauses dispatch instead. The engine never dispatches on mixed code, and it never pulls: updating is always an operator act (git pull, or gateline self-update). Unmerged changes are tried from their own worktree with gateline ui, never up, so unreviewed code is never put in charge of live, metered dispatch.

Autonomy is earned, and measured

The framework treats orchestration autonomy the way you would treat a new hire's: v0 exists so the team builds a calibrated sense of where agents are strong before granting routing autonomy. The promotion criterion is explicit (gate reviews have become confirmations rather than corrections), and it is measured: the gate frontend records a burden (confirmation, light-correction, or heavy-correction) in the act of deciding, and hand-recorded decisions are expected to set it, so the criterion is checkable from the record where decisions flow through the frontend, while older or hand-entered decisions may lack it.

Autonomy arrives across a milestone ladder, and the ladder has a gate of its own. M0 lands the contracts and a hand-kept ledger. M1 runs the engine in shadow mode (a dry-run tick against finished v0 runs, comparing each derived action to what the human orchestrator actually did); the bar was three full runs with every disagreement dispositioned as an engine bug or a design finding. M2 is the autonomous loop on one vendor, and it could not begin until the promotion criterion was credibly met: a sustained majority of confirmation burden across v0 gate decisions. M2 has since run: the engine has driven real runs against this repository, creation-seam among them, with humans acting only at gates and escalations. M3 adds cross-vendor dispatch with the vendor pins enforced at dispatch time; M4 hardens crash recovery and isolation, and per-task worktrees, session resume, and crash recovery are in place.

The shadow record shows the bar working as design review. Against the first v0 run the engine's derived action matched the human's on 8 of 19 steps, and the disagreements traced to bookkeeping gaps the design then closed; by the later runs agreement reached 18 of 22 and 16 of 23 steps, with the remaining disagreements specific, fixable defects. Autonomy here is not a switch that ships: it remains gated on the measured criterion, and no mode of the framework is "fully autonomous."

Autonomy as a ladder with a gate. Each milestone has an exit criterion; the autonomous loop (M2) waits on a sustained majority of confirmation-burden gate reviews.
Trust ladder: M0 to M4 in ascending steps, with an autonomy gate barrier between M1 and M2, measured by the burden metric. M0 contracts land hand-kept ledger M1 shadow mode: derived action vs. human action over 3 finished v0 runs M2 autonomous loop, one vendor M3 cross-vendor dispatch; vendor pins enforced M4 hardening: crash recovery, isolation, packaging autonomy increases; gates stay human autonomy gate M2 does not begin until gate reviews are mostly confirmations measured by burden: confirmation · light-correction · heavy-correction evidence so far: 8/19 → 18/22 → 16/23 steps agreed (wordfreq → mdtoc → dupefind)

The third page covers the other half of the trust story, how gateline stays vendor-neutral, so no model or harness is baked into the pipeline. For the decision commands themselves, see the CLI reference; for every field the two writers share, the state.yaml reference.