Saphan StudioDocs
Reference/Command reference

saphan fleet

Fleet views over the session registry

saphan fleet

Fleet views over the session registry

saphan fleet

Subcommands

SubcommandWhat it does
saphan fleet dispatchableOne row per seat in the registry: may dispatch, refused, or unmeasured — with exactly one named obstacle
saphan fleet headroomProbe fleet capacity with per-backend model lists — a report, never a dispatch (§1.1)
saphan fleet healthCount fleet work failures without counting probe runs as failures
saphan fleet legsOne row per leg the RECORD says is running — every column names the instrument that answered it
saphan fleet nextOne read: the stream's state, EVERY gate it still owes, the legal next acts, and a ready command per act; --rules answers how dispatch works in this project
saphan fleet observeRun the git observation for awaiting-observation streams and report what it found
saphan fleet showShow the resolved fleet + the one notification (§3.1, §3.3)
saphan fleet usageRead every seated seat's own usage fact — a report, never a dispatch (§1.1)

saphan fleet dispatchable

One row per seat in the registry: may dispatch, refused, or unmeasured — with exactly one named obstacle

saphan fleet dispatchable

Answer, for EVERY seat the registry carries, one question: may a run be dispatched onto it right now? The answer is one of THREE verdicts, never two:

may-dispatch · refused · unmeasured

MAY DISPATCH requires positive readings on every rung. REFUSED means a MEASURED obstacle was found. UNMEASURED means the column that decides this seat WAS NEVER READ — no obstacle was found ON THAT COLUMN and none was authorised. It is NOT a finding that the seat is otherwise clear: exactly one obstacle is printed per row, the FIRST rung that answered, so a lower rung may still hold a measured obstacle this row does not print.

THE THIRD VERDICT EXISTS BECAUSE TWO WERE A LIE. This report once printed 0 may dispatch, 11 refused for a fleet that was dispatching on three machines: all eleven were the same absent reading, and "refused" is a claim to have FOUND an obstacle. A master reads that as "there is nowhere to run". UNMEASURED admits nobody — it renames a true state, it does not open a gate.

Refused and unmeasured seats BOTH name exactly ONE obstacle from a closed set, and the verdict says which kind of answer produced it:

retired · role-not-executor · slots-full · credential-dead · headroom-exhausted · headroom-unknown · confinement-unavailable

TOTAL BY CONSTRUCTION. |output| == |seats in the registry|, always. The registry is enumerated and MAPPED over — never filtered — because a seat with no row is exactly how this fleet lost three working codex seats: there was no row, so there was no seat. Both counts ride in --json so a consumer can check the invariant itself.

NEVER INFERS FREE FROM SILENCE. "May dispatch" requires POSITIVE readings: a measured-capable confinement on the machine, room in the slot, and a positive, well-stamped fact on BOTH fact columns — headroom-ok AND credential-ok. Where no fact exists on either, the verdict is unmeasured under the obstacle word headroom-unknown, carrying the unread reading class and a sentence naming WHICH column nobody looked at — a CORRECT result, not a missing one, and the exact opposite of the defect this verb exists to kill (an absent reading read as a reading). An unread credential column is NEVER reported as credential-dead: that word is reserved for a MEASURED failure to log in.

This verb is the READER of the seat-fact contract, never its writer:

seat · verdict · as_of (UTC, Z) · source · run_ref · detail

Pure read: no reconcile, no fleet resolve, no store write, and NO VENDOR PROBE — it spawns nothing and costs nothing, so it is safe in a loop. saphan fleet headroom is the verb that actually measures a lane, and it bills for it.

The same projection is rendered for the owner at /console/fleet — one composition, two renderers (PRESENTATION_CONTRACT_V1 §12).

saphan fleet headroom

Probe fleet capacity with per-backend model lists — a report, never a dispatch (§1.1)

saphan fleet headroom [flags]

Probe every applicable place (backend on a local harness, admitted by an applicability predicate — never a Machines list, D-A) with that backend's configured ordered model list (or one explicit --model override), and report which ones are free RIGHT NOW (CAN-NOW) beside what the identity store already knows they can do (CAN-DO, read-only — this verb never writes a capability.Record, never a lease, never a rung).

Law #8, final form: a place counts as free only on a POSITIVE assertion over its OWN probe run — run_record.status == "ok", a non-empty capture, and the literal LANE-OK inside it (absence of a negative is never a pass); each probe's run_record.model must equal the model named for that attempt (capacity is per-model); the probe's wall-clock is never below an 8-minute floor (a tight limit fabricates a killed-timeout a naive check later misreads as clean); and the verdict reads the ENGINE ROW's status/exit_status, never the capture alone.

This verb never dispatches and never selects a winner — it prints a list; the choice of where to run stays a human act (§1.1).

Every probe this verb runs is itself a real, billable spawn through the SAME engine door every other run uses — and it is a DELIBERATELY EXEMPT CLASS on two of that door's rungs (order fleet-headroom-caller P7/N5): it never queues behind R8, the STREAM dispatch lock (every place probed in one invocation shares one synthetic stream by construction, so locking it would refuse the second place's probe on the first's still-live row), and it is never itself refused by R17's fleet-capacity gate (a probe's whole purpose is to re-measure whether a lane a PRIOR measurement called dead has come back alive — gating it on that same prior verdict would permanently close the fleet's own ability to re-check). Both exemptions are named on every probe's own run_record enforcement_note, not merely silent absences. R8c, the per-(machine, slot) lock, is NOT among them: a probe names no machine and no slot, so that rung is already a no-op for it, and waiving it would only have let anything claiming this stream name walk past an atomic slot reservation.

Options

OptionTypeDefaultDescription
--actorstringunsetmandate holder's id (falls back to env SAPHAN_ACTOR, then ~/.saphan/config's identity.actor)
--backendstringunsetrestrict the probe to one backend: claude-code | codex (omitted = every applicable local backend)
--estimate-usdfloat640the owner-quoted cost estimate this report is measured against (required)
--modelstringunsetexplicit one-model override for every backend (omitted: probe.model.<backend>, then documented defaults)

saphan fleet health

Count fleet work failures without counting probe runs as failures

saphan fleet health [flags]

Read terminal run_record statuses over a UTC window. The headline is work only: probe runs are excluded by the persisted purpose field, never by a stream-name pattern. Historical rows that predate the field remain visible as unclassified and are excluded from the headline rather than silently treated as work.

Options

OptionTypeDefaultDescription
--windowduration2h0m0slookback window ending now (for example 2h or 24h)

saphan fleet legs

One row per leg the RECORD says is running — every column names the instrument that answered it

saphan fleet legs

List the legs currently running, one row each, with the source of every column stated beside its value:

stream · seat · machine · model · start · elapsed · budget cap · spent-so-far · liveness

The liveness column NEVER prints a single fused word. The record (a run_record row with status=running and no ts_ended) and the process table (kill(pid,0) on this host) are two instruments that are known to disagree — the founding observation is legs visible in the process table while the record says otherwise. Where they agree, this verb says AGREE and names both. Where they disagree, it prints BOTH readings and says which instrument answered which; it never silently prefers one. Where an instrument cannot answer — a transport=ssh row's pid is the LOCAL orchestrator's, so a local probe measures THIS host and is silent about the host doing the work — it says NOT MEASURED HERE and names why.

Pure read: no reconcile, no fleet resolve, no store write. The mandate columns (budget cap, the order's declared model) come from the event log AS IT ALREADY STANDS — a bullet written since the last reconcile is not visible here; saphan bus reconcile is the verb that refreshes it.

Rows are enumerated FROM THE RECORD. A process running with no run_record row cannot appear — this verb probes pids it was given, it does not sweep the process table.

saphan fleet next

One read: the stream's state, EVERY gate it still owes, the legal next acts, and a ready command per act; --rules answers how dispatch works in this project

saphan fleet next --stream <slug> | --rules [flags]

Project one stream's "what now" in a SINGLE read: the resolved state, the COMPLETE set of gates still owed forward to a terminal, every act the transition table makes legal from here with a paste-ready command, and every act it does NOT with that row's own refusal text. The previous run's recorded argv prints beside them, absence included.

Completeness is the substance. saphan dry-run merge reveals gates ONE AT A TIME because each invocation asks about one act; this verb walks the transition table (internal/routing/transitions.go — the same rows the refusals are computed from) and answers the whole question at once. It owns no state set of its own: an eighth hand-written set is the disease the table exists to cure.

Every printed command is literal and cwd-independent — absolute paths, explicit --workspace, executable from ANY directory. Gate acts print one complete line per legal decision and omit --actor so config resolves the canonical identity. If another required human input is absent, the tray names that absence instead of printing a command with a hole in it.

A stream awaiting a human gate exits 0 — the divergence from fleet show's exit 2 is deliberate: a projection that fails exactly when it is most needed is useless as a read. An unknown slug still exits 2.

--rules is the SECOND section of this same verb (tray-next-rules, owner act 2026-08-07: "one verb" — not a second werb): how dispatch works in this project, projected from the live transition table, the machine registry, and the toolchain grant boundary — never a hand-written digest of the playbook. --rules runs WITHOUT --stream and exits 0; passed together with --stream, the payload carries both sections.

Options

OptionTypeDefaultDescription
--rulesboolfalsealso project the dispatch-rules section (runs without --stream)
--streamstringunsetthe stream slug to project

saphan fleet observe

Run the git observation for awaiting-observation streams and report what it found

saphan fleet observe [<stream>] [flags]

Run the merged observation for every awaiting-observation stream (or the one named) and report per stream what the read-only probe found:

observed-merged a real merge was observed; it is in the durable record (observe once, remember forever — adr-0018) not-merged git answered definitively: no merge yet — merge when ready; the engine only observes (§1.7) cannot-observe the question is currently unanswerable — the reason and a paste-ready manual probe are printed

Ancestry is answered in the stream's OWNING repo, which the engine captures from the live worktree's .git linkage at ingest and keeps in the event log — so the probe still resolves after teardown, cross-repo owners included. For streams ingested before that capture existed, --repo backfills the pointer:

saphan fleet observe <stream> --repo <owning-repo-path>

records the path once (an explicit operator statement, never a guess); every later probe uses it. The verb writes no git state and decides no gate: observations are recorded facts (§1.7), gates stay yours (§1.1).

Options

OptionTypeDefaultDescription
--repostringunsetbackfill the named stream's owning-repo pointer (absolute path; recorded once, used by every later probe)

saphan fleet show

Show the resolved fleet + the one notification (§3.1, §3.3)

saphan fleet show [flags]

Project the resolved fleet: one row per stream with the state resolved through all three trigger layers — I (ingested channel artifacts, §2.4), H (human gate decisions from the fleet_gate log) and G (observed git, §1.7) — followed by the one notification (§3.3). A stream awaiting a human gate makes the report actionable: exit 2.

--json defaults to detail=summary (stream/state/valence/last_gate/liveness, last_gate carrying no signature blob); --detail full adds evidence_recipe/ phase_spans/merge_message/last_gate.sig. --state/--since/--streams narrow which rows render, on both --json and the table (rider mcp-contract klasa #2, STOP1_PLAN Decision 6) — total_count in --json always names the fleet's real size, even when narrowed. --changed-since is the heartbeat-poll cursor (RS-5): pass back a prior --json response's as_of_ts to see only what changed. --json also carries machines — the machine × identity × role × pulse projection (RS-0b's admitted machines, RS-5).

Options

OptionTypeDefaultDescription
--changed-sincestringunsetheartbeat-poll cursor (RS-5): narrow to streams that changed at or after a PRIOR response's as_of_ts
--detailstringunsetsummary (default: stream/state/valence/last_gate/liveness) | full (adds evidence_recipe/phase_spans/merge_message)
--sincestringunsetnarrow to streams whose ingest updated_ts is at or after this RFC3339 timestamp
--statestringunsetnarrow to streams whose resolved state renders exactly this (e.g. "in-flight", "gated:accept")
--streamsstringSlice[]narrow to these stream slugs (comma-separated, or repeat the flag)

saphan fleet usage

Read every seated seat's own usage fact — a report, never a dispatch (§1.1)

saphan fleet usage [flags]

Run the vendor's own /usage probe on every seated identity (registry.Machines. Identities), binding each probe's CLAUDE_CONFIG_DIR to THAT seat's own home through the existing seat-cast fields (runner/seatenv.go, runner/ssh.go) — never the ambient profile "fleet headroom" probes (STOP1_Plan §6 names that verb's own gap: it probes one sample, not the fleet).

The vendor's result text has no schema, no version, and no contract (internal/runner/parse.go's own posture). Claude seats use the vendor parser; Codex seats use the rollout-backed headroom fact reader, which carries a percent and reset instant when the event provides them. Both arms return a named absence rather than a silent zero.

A seat seated for a backend other than claude-code or codex renders "unsupported" BY NAME. A Codex seat is readable without a vendor probe: its rollout files are read locally or through the existing SSH transport.

Every Claude probe invokes the registered vendor binary directly as --output-format=json -p /usage under the seat's own CLAUDE_CONFIG_DIR. The vendor answers this local read with zero model turns and zero dollars. The vendor JSON's result field reaches internal/usage unchanged, while the run still writes the same run_record + cost-ledger rows as every other probe.

This verb never dispatches and never selects a winner — it prints a list; the choice of where to run stays a human act (§1.1).

Options

OptionTypeDefaultDescription
--actorstringunsetmandate holder's id (falls back to env SAPHAN_ACTOR, then ~/.saphan/config's identity.actor)
--machinestringunsetrestrict the probe to seats on one machine (omitted = every seated identity)
--modelstringclaude-haiku-4-5-20251001compatibility report label only — the zero-turn /usage read invokes no model
--slotint0restrict the probe to one slot on --machine (requires --machine)

On this page