Saphan StudioDocs
Getting started

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:

saphan gate <stream> --gate <which> --decision <what> [--ref <review-return>] [--note <detail>]
  • --gate is one of: stop1 · stop2 · merge · promotion · review — required, no default. teardown is 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 teardown refuses here and prints the equivalent saphan teardown command with your --actor, --note and --return arguments already folded in. See “The teardown decision has one road”, below.
  • --decision is 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 merged decision. 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-run validates 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:

saphan gate batch /absolute/path/to/decisions.json --actor marcin --dry-run
saphan gate batch /absolute/path/to/decisions.json --actor marcin

The file is one JSON object. Every decision is complete on its own; there is no shared or default gate, decision, or note:

{
  "decisions": [
    {
      "stream": "alpha",
      "gate": "stop2",
      "decision": "accept",
      "note": "review return alpha-17 accepted",
      "recommendation_ref": "alpha-review-17"
    },
    {
      "stream": "beta",
      "gate": "stop2",
      "decision": "conditions",
      "note": "land the two named review conditions"
    }
  ]
}

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:

  1. Build the file with one explicit object per stream. Do not reuse an old decision by omission: gate and decision are mandatory on every object.
  2. Run the batch with --dry-run. Read every per-stream preview or refused outcome; the dry-run performs one reconcile and writes no fleet_gate row.
  3. 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.
  4. Run the same file without --dry-run. The engine reconciles once, then records one separately signed row per admitted stream.
  5. Read the final named outcome. complete means every item recorded (or already stood in the record). partial means some items recorded and others refused or were not attempted; refused means 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.
  6. Verify any row independently with saphan gates --stream <stream>; an ok verdict 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.

On this page