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. (
--dry-runvalidates without writing.)
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. Every item requires a non-empty note, so the
ground remains per stream and inside that row's signed bytes. 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. - Run the batch with
--dry-run. Read every per-streamprevieworrefusedoutcome; the dry-run performs one reconcile and writes nofleet_gaterow. - 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 without
--dry-run. 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> remains 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 or merge input; the
live conflict probe above is.
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 ·
consumed · discarded-by-decision). Teardown removes nothing itself — it prints the
worktree-removal command as the operator's own last step. Closing a stream's workspace and
recording that its work landed are two decisions, and collapsing them destroys the record
of the stream's ending.
The teardown decision has one road
saphan gate <stream> --gate teardown used to record the same signed row that
saphan teardown records. The two were not the same act. Everything the teardown verb does
first — archiving the channel family, the return manifest, .provision/ and the provisioned
bytes, and checking that every provisioned citation on the stream's run record still
resolves to bytes that exist — happened on one of the two roads. Four owner-signed
teardowns went down the other one.
So --gate teardown now refuses, and prints the saphan teardown command that performs
the same decision with its preconditions. Nothing is withdrawn: the teardown verb takes the
same --actor, --note, --return and --dry-run, records the identical master-class
row, and adds the archive commandment 01 requires before any teardown. Your signature means
one thing again.