saphan fleet
Fleet views over the session registry
saphan fleet
Fleet views over the session registry
Subcommands
| Subcommand | What it does |
|---|---|
saphan fleet dispatchable | One row per seat in the registry: may dispatch, refused, or unmeasured — with exactly one named obstacle |
saphan fleet headroom | Probe fleet capacity with per-backend model lists — a report, never a dispatch (§1.1) |
saphan fleet health | Count fleet work failures without counting probe runs as failures |
saphan fleet legs | One row per leg the RECORD says is running — every column names the instrument that answered it |
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 observe | Run the git observation for awaiting-observation streams and report what it found |
saphan fleet show | Show the resolved fleet + the one notification (§3.1, §3.3) |
saphan fleet usage | Read 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
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)
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
| Option | Type | Default | Description |
|---|---|---|---|
--actor | string | unset | mandate holder's id (falls back to env SAPHAN_ACTOR, then ~/.saphan/config's identity.actor) |
--backend | string | unset | restrict the probe to one backend: claude-code | codex (omitted = every applicable local backend) |
--estimate-usd | float64 | 0 | the owner-quoted cost estimate this report is measured against (required) |
--model | string | unset | explicit 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
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
| Option | Type | Default | Description |
|---|---|---|---|
--window | duration | 2h0m0s | lookback 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
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
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
| Option | Type | Default | Description |
|---|---|---|---|
--rules | bool | false | also project the dispatch-rules section (runs without --stream) |
--stream | string | unset | the stream slug to project |
saphan fleet observe
Run the git observation for awaiting-observation streams and report what it found
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
| Option | Type | Default | Description |
|---|---|---|---|
--repo | string | unset | backfill 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)
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
| Option | Type | Default | Description |
|---|---|---|---|
--changed-since | string | unset | heartbeat-poll cursor (RS-5): narrow to streams that changed at or after a PRIOR response's as_of_ts |
--detail | string | unset | summary (default: stream/state/valence/last_gate/liveness) | full (adds evidence_recipe/phase_spans/merge_message) |
--since | string | unset | narrow to streams whose ingest updated_ts is at or after this RFC3339 timestamp |
--state | string | unset | narrow to streams whose resolved state renders exactly this (e.g. "in-flight", "gated:accept") |
--streams | stringSlice | [] | 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)
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
| Option | Type | Default | Description |
|---|---|---|---|
--actor | string | unset | mandate holder's id (falls back to env SAPHAN_ACTOR, then ~/.saphan/config's identity.actor) |
--machine | string | unset | restrict the probe to seats on one machine (omitted = every seated identity) |
--model | string | claude-haiku-4-5-20251001 | compatibility report label only — the zero-turn /usage read invokes no model |
--slot | int | 0 | restrict the probe to one slot on --machine (requires --machine) |