Gates: where humans decide
One recorded human decision on one stream, the batch surface, and the verbs around it.
A gate is one recorded human decision on one stream. The gate verb is the only writer of gate rows, and its shape enforces the philosophy:
--gateis one of:stop1 · stop2 · merge · promotion · review— required, no default.teardownis not recorded by this verb. It is the same signed row, but it belongs on top of an archive this verb does not write, so--gate teardownrefuses here and prints the equivalentsaphan teardowncommand with your--actor,--noteand--returnarguments already folded in. See “The teardown decision has one road”, below.--decisionis one of:accept · conditions · reject · ready-to-merge · closed-without-merge— required, no default. A defaulted decision is unrepresentable at the surface.- The actor is the human deciding. The engine is never an actor.
- There is no
mergeddecision. Merged is only ever observed from a real merge commit — you cannot type a stream into the merged state. - A decision that would skip a gate in the required order is refused with a list of what is
still owed, and nothing is written on refusal.
saphan dry-run gate …runs the same checks and shows the row it would write, without writing it. - An owner-class gate (
stop1,stop2,merge,promotion) is an owner act: it asks for a one-time six-character code typed back on your terminal. Where the workspace requires a person's signature on gates, the row is signed with your passkey in the console or, with--cli, with a security key at the terminal. Areviewgate records the reviewer's judgment and asks for none of these.
Recording a batch of owner decisions
When several streams are ready for a decision, one owner gesture can record them without paying the fleet-wide channel reconcile once per stream:
The file is one JSON object. Every decision is complete on its own; there is no shared or default gate, decision, or note:
This surface is for owner-class gates only; a review item is refused. Every item requires a
non-empty note, so the ground remains per stream and inside that row's signed bytes. The real
run asks for the one-time code once, on a screen that lists every decision it covers. A deliberate correction
behind a later gate additionally carries "out_of_order": "<why>" on that item.
The owner ceremony is:
- Build the file with one explicit object per stream. Do not reuse an old decision by
omission:
gateanddecisionare mandatory on every object. - Rehearse it with
saphan dry-run gate batch <file>. Read every per-streamprevieworrefusedoutcome; the rehearsal reads fresh agent activity once and records no gate. - Correct refused inputs or, when the refusal is intentional and the named guard permits
it, add that stream's explicit ground such as
out_of_order. - Run the same file with
saphan gate batch <file>. The engine reconciles once, then records one separately signed row per admitted stream. - Read the final named outcome.
completemeans every item recorded (or already stood in the record).partialmeans some items recorded and others refused or were not attempted;refusedmeans none recorded. Both non-complete outcomes exit 2 and list every stream, so retry only the refused/not-attempted objects. Already-recorded items are safe but reported rather than silently duplicated. - Verify any row independently with
saphan gates --stream <stream>; anokverdict is the per-row signature check.
The batch is deliberately not a collective decision and not a rollback transaction. A later refusal cannot erase earlier human acts: successful rows remain visible, and every item receives a named result.
Around the gate verb, three companions:
saphan fleet next --stream <slug> answers, in one read: the stream's current state,
every gate it still owes, the legal next acts, and a ready-to-paste command for each. This
is the "what do I do now" verb — it walks the same transition table the refusals use, so
the affordances and the refusals can never disagree.
saphan merge <stream> executes the engine-led merge of a gated stream — it checks the
preconditions (the gates actually recorded, the review lane satisfied when engaged),
then materializes the live branch+trunk pair in a detached scratch worktree and runs a
suite-free conflict probe. A conflict refuses and names the paths; a clean probe lands in
the same serialized engine act and records what it did. Two concurrent merge acts wait in
the engine and each probes the trunk it actually lands on. It is the one place mechanics
touch the merge, and it runs only after the human's decision exists.
saphan verify-union <stream> is an advisory full-suite instrument. It records
what the merged pair does under the deterministic suite and --status reports whether
either recorded parent moved. That record is not a merge precondition: saphan merge writes
the union verdict for the pair into its record but does not refuse on it; the live conflict
probe above is what stops a merge. ⚠ verify-union refuses to measure until the suite's time
limit is declared (saphan config set verify.full.timeout <duration>).
It runs the suite you declared (see Declaring the verification suite below), and it
reports the count the same way the certification record does — so under the exitcode
contract the line reads union clean-green — unavailable test(s) executed (report exitcode, suite config:verify.full.argv …) and the --json payload carries
"count": "unavailable". It never prints 0 where nothing counted. A conflict prints
no count at all: git could not combine the parents, so the suite never ran and there is no
verdict about the code — the line says exactly that, and the --json record answers
"count": "unavailable" and "named_tests": "unavailable" for the same reason, never 0
and never none.
saphan teardown <stream> archives the stream's channel artifacts (verified by
checksums, into the stream's own work directory) and records the teardown gate — and
refuses until the projection actually shows the stream merged and the worktree is clean of
unaccounted files, with every return artifact's fate explicitly dispositioned (landed ·
landed:<path> · consumed · discarded-by-decision). It is an owner act. Recording the
decision removes nothing: the worktree is removed by a second, separate act,
saphan teardown <stream> --verify-archive, which re-verifies every archived byte first and,
for a worktree on a remote machine, brings that machine's leftovers home before removing it.
Both acts can be rehearsed with saphan dry-run teardown.
The teardown decision has one road
The teardown gate is recorded only by saphan teardown, because the row belongs on top of an
archive: the channel family, the return manifest, .provision/ and the provisioned bytes are
archived first, and every provisioned citation on the stream's run record is checked to still
resolve to bytes that exist. saphan gate <stream> --gate teardown refuses, and prints the
saphan teardown command that performs the same decision with its preconditions, with your
--actor, --note and --return arguments folded in.
Gates are one of several acts that need the owner present. The full list, with a runbook for each newer one, is in Acts that need the owner present.
Placing a leg: from an order to a running agent
The operator's walk for starting one leg of work with a single command: print the order template, fill it, commit it, and place it on any free seat.
Acts that need the owner present
The verbs that refuse unless the owner is present, and a numbered runbook for each: what to run, what success prints, and what to do when one refuses.