saphan run
Spawn one mandated run through the engine's spawn door (runner-v1; operator verb)
saphan run
Spawn one mandated run through the engine's spawn door (runner-v1; operator verb)
Spawn a process under mandate and record the run: one door for agent sessions and arbitrary executables alike — one mandate enforcement, one transcript discipline, one cost ledger, one identity seam.
Backends (CONTRACT_runner.md §5, PLAN_OPENAI_COMPAT.md, PLAN_agent-loop_v0): proc (substrate — any executable), claude-code (headless Claude Code CLI), codex (headless Codex CLI), openai-compat (in-process HTTP to a local craftsman speaking the OpenAI chat-completions dialect — LiteLLM proxy, or Ollama/vLLM directly; --base-url replaces --binary, which this backend does not use), agent-loop ('saphan-agent loop' — our own tool-calling agent loop, no vendor binary on its critical path; --lane names the config-ladder alias whose credential it resolves on ITS OWN execution host, --max-tokens/ --max-iterations/--inner-budget mirror its own flags). The model is mandatory for LLM backends and set by the runner, never left to session defaults; effort is enforced where the backend exposes it and its absence is recorded, never silently defaulted (agent-loop exposes no wire field for it at all, same class as openai-compat).
Refusals R1–R7 are RECORDED RunRecords (exit 3), never fallbacks: a plan-profile dispatch to a backend without two-pass refuses (R1, including openai-compat — chat completions are stateless); ephemeral modes are illegal for two-pass (R2); a resume targets the recorded session key only (R3, openai-compat has none to target); a missing model or a pinned-digest mismatch refuses (R4); bypass flags are outside the mandate vocabulary (R5); an effort-strict mandate refuses on codex until probe P4 lands, and always on openai-compat, which has no wire field for effort (R6); write access outside the workdir without a signed disposition refuses (R7, adr-0020); a --config permission class's commands.allow, present only for proc, refuses a binary+argv it does not list (R9) — a class only NARROWS what proc could already do, it never grants (T1 deny-by-default; internal/permissions). A schema-v2 class's Path/EnvMint is not R9's proc-only narrowing: it mints PATH and any declared env_mint into EVERY backend's composed environment, uniformly (spawn-capability-reach §1, adapter.go's compose() chokepoint).
--laws names the standing-law index every LLM spawn must deliver to the head (headless-law-injection L0): a headless dispatch fires no SessionStart hook of its own, so the engine reads the file FRESH at every spawn — including every resume — inlines it into the prompt, and records its SHA-256. A file that is missing, unreadable, not a regular file, or ZERO BYTES refuses R12 (recorded, exit 3, no process spawned): a head that cannot be handed its laws is not dispatched at all — an empty floor is not a floor. An OMITTED --laws on an LLM backend is a malformation (exit 1, the same class as an omitted brief), not a refusal; --laws on proc is unrepresentable (there is no head to bind).
Spawn authority (order §3): v0 = THIS operator verb. Timestamps are machine
clock only. Captures land under .saphan/work/<stream>/runs/<run-id>/ with
an engine-written gitignore cover. Secret VALUES never appear on any
surface (§1.6): --env passes variable NAMES through, values are read at
spawn and stored nowhere.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--actor | string | unset | mandate holder's id — never the process identity (falls back to env SAPHAN_ACTOR, then ~/.saphan/config's identity.actor) |
--allow-unknown-binary-provenance | bool | false | transport=ssh only: proceed when the remote binary's digest could not be established (no sha256sum/shasum on the far host, or the identity probe's dial failed) — the record still carries binary_sha256 "unknown", never a silent local substitute (R13); does NOT waive a confirmed absence (the binary does not exist, or is a directory, on the far host) |
--arg | stringArray | [] | proc argv entry (repeatable, ordered) |
--backend | string | unset | backend: proc | claude-code | codex | openai-compat | agent-loop | qwen-code (required, no default) |
--base-ref | string | unset | base branch name measured by remote admission-repo freshness checks (normally engine-composed from the order) |
--base-url | string | unset | openai-compat only: endpoint root (LiteLLM-proxy root or Ollama/vLLM root); /chat/completions is appended (required for openai-compat) |
--binary | string | unset | absolute path (resolved on this host) or $HOME/… (resolved on the execution host for transport=ssh, on this host otherwise) — PATH resolution is forbidden (required, except openai-compat, which has none) |
--binary-sha256 | string | unset | pin the binary digest; a mismatch refuses (R4 — no silent substitution) |
--branch | string | unset | stream branch this dispatch cuts its worktree on — the ssh dispatch-out act far side, git worktree add in the stream's own repository local side; absent = no worktree is created and the workdir must already exist (normally engine-composed from the order) |
--brief | string | unset | kickoff brief path for LLM backends (the path rides in the prompt, §7-S3) |
--budget-usd | float64 | 0 | claude-code only: --max-budget-usd limit |
--config | string | unset | permission class (schema v1 or v2, internal/permissions): inline JSON, or with --file a path to a named class file (T2: production classes are named files) — commands.allow narrows what a proc spawn may run (R9, proc-only); a v2 entry's path/env_mint mints PATH/env into EVERY backend's spawn, not just proc; absent = no class, today's behavior unchanged (T1 deny-by-default) |
--contact | string | unset | WHO TO ASK about this stream — the order's contact: header line, verbatim; a ROUTING HINT and an unverified claim, never an identity and never an input to any authorisation or gating decision (--actor is who the run runs as); omitted records NO DECLARATION |
--cost-estimate-usd | float64 | 0 | OPTIONAL owner-quoted cost estimate for this run; absent = quoted stays NULL, never invented (honest-unknown, contract §7-S1); recorded on the run and read back as quoted/actual/drift by saphan cost |
--dry-run-arg | stringArray | [] | proc: declared dry-run argv entry — declares the two-pass capability (repeatable) |
--effort | string | unset | mandate.effort — enforced where the backend exposes it; absence recorded |
--effort-strict | bool | false | refuse when the backend cannot enforce effort (R6; codex until probe P4) |
--egress-policy | bool | false | declare that this run is judged by the fleet's egress policy document (<state-dir>/egress/policy.json, the file the console's egress page edits). The run's outbound network is then routed through a per-run SOCKS5 door this engine mints, serves and unlinks, and the policy's compiled digest lands on the run's record. REFUSES BEFORE THE SPAWN, at $0, if the document is absent, unreadable or malformed, if the host has no measured egress rung, or on any lane that cannot carry a door (ssh, vendor-delegated confinement, a container spawn) — it never starts quietly with an open network. Omitted = UNCHANGED behavior: no policy, no cut, no door, and the same open network every spawn on this build gets |
--env | stringArray | [] | environment variable NAME to ADD to the backend's baseline env (repeatable, additive-only — never removes the baseline; values are never recorded, §1.6) |
--ephemeral | bool | false | no-persist vendor mode; ILLEGAL for plan profile (R2) |
--file | bool | false | treat --config's value as a path to a named class file rather than inline JSON |
--force-dyad | bool | false | override R8d's (stream, role) dispatch lock — an owner ACT, never a silent bypass: the refusal R8d would have returned is appended onto this run's own record (enforcement_note) before the spawn proceeds |
--git-user-email | string | unset | git author/committer EMAIL this leg commits as, injected as GIT_AUTHOR_EMAIL/GIT_COMMITTER_EMAIL (must be given with --git-user-name); omitted = filled from config-v1's run.git_user_email |
--git-user-name | string | unset | git author/committer NAME this leg commits as, injected as GIT_AUTHOR_NAME/GIT_COMMITTER_NAME (must be given with --git-user-email); omitted = filled from config-v1's run.git_user_name, and an empty pair REFUSES at R50 on a far or seated spawn rather than letting the leg commit as its own host |
--headroom-preflight | bool | false | probe the fleet for a free place BEFORE this spawn (R17) and feed the result into this run's own headroom gate — OPT-IN: costs ~$0.06 per applicable place plus each place's own wall-clock (law #8 property 3's 8-minute floor); omitted = today's behavior, Deps.Headroom stays nil for this run |
--identity | string | unset | cast-surface: executor identity seated on --machine whose CLAUDE_CONFIG_DIR binds this session; omitted = auto-resolved iff --machine carries EXACTLY one seated identity, else refused (never guessed) |
--inner-budget | duration | 0s | agent-loop only: explicit inner budget under --wall-clock (0 = derived from it, §1.6) |
--lane | string | unset | agent-loop only: config-ladder lane alias whose credential the child resolves on ITS OWN execution host (never composed into this spec, §1.6) |
--laws | string | unset | ABSOLUTE path to the standing-law index the head must receive — REQUIRED for LLM backends; its content is inlined into the prompt at every spawn and its SHA-256 is recorded (R12 refuses an unreadable file) |
--machine | string | unset | cast-surface: admitted machine id to run this spawn on — the TRANSPORT comes from that machine's own registry row (ssh dials it, local runs it right here, RS-2b + order local-seats), never from this flag; omitted = no machine, no seat, today's plain local behavior unchanged; "auto" (order machine-auto-placement, Q1) hands placement to the engine — dispatch.Resolve picks the seat (capability rung, then free-live, then price) and reserves it atomically before this run ever spawns, never with --identity/--slot |
--max-iterations | int | 0 | agent-loop only: cap on model turns (0 = the loop's own default of 25) |
--max-tokens | int | 0 | agent-loop only: per-request --max-tokens (0 = the endpoint's own default; R18 refuses an explicit value under 256) |
--model | string | unset | mandate.model — mandatory for LLM backends (order §4.1) |
--no-channel-skeleton | bool | false | do NOT emit the role-owned channel skeleton into --workdir before spawn (default: emitted, rider M1) — for benchmark/non-fleet runs, where a fleet-dispatch artifact in the workdir pollutes the measurement; fleet dispatch (serverd) is unaffected, this flag exists only at this operator verb |
--outside-write-dir | stringArray | [] | request write access outside the workdir — refuses without a signed disposition (R7, adr-0020) |
--profile | string | unset | capability profile: plan | execute (required, no default) |
--provision | stringArray | [] | provision <source>=<workdir-relative-dest>, where <source> is a file or a DIRECTORY (repeatable; a directory is walked sorted and every member lands, the executable bit survives; a file is recorded as dest@sha:<12>, a tree as dest@tree:<manifest digest>:<members>) |
--require-capability | stringArray | [] | capability required by this work (repeatable; e.g. loopback-listener) |
--require-egress-cut | bool | false | refuse to start unless the machine this run lands on has a MEASURED egress lever (machine probe-confinement records the ladder: netns -> cgroup v2 -> separate uid on Linux, seatbelt (deny network*) -> separate uid on macOS). A host with no rung refuses BEFORE the spawn, class egress-unavailable — it never starts quietly with an open network. Omitted = the ADMISSION question is not asked, and it is not the same as the network being open: the cut itself follows the run's EGRESS POLICY, not this flag, so a run that declares one is walled in either way and a run that declares none gets the open network either way. What this flag adds is the refusal BEFORE the spawn on a host measured to have no rung |
--resume | string | unset | resume the RECORDED vendor session key (two-pass PASS 2; R3: never a fork, never --last) |
--reviewed | string | unset | the BARE slug of the stream this spawn REVIEWS (no -review, no -rN suffix) — REQUIRED with --role review and refused with any other role (R52); it is substituted into the injected _REVIEW_STATUS.md H1 rule, so the leg derives nothing: both fleet patterns are representable (a leg on its own <reviewed>-review stream, and a leg spawned into the reviewed stream's own worktree, where this equals --stream) |
--role | string | unset | fleet role this spawn carries: master | executor | review | design | scribe | advisor (spec.Validate's closed vocabulary, READ from it — never retyped here); omitted = no role, R8d's dispatch lock is a no-op for this spawn (there is nothing to key it on) — but R25's placement check is NOT a no-op: an UNRESERVED seat (registered executor, the default) still places it, a RESERVED seat (registered anything else) REFUSES it exactly as it refuses a mismatched role |
--slot | int | 0 | cast-surface: slot index on --machine to reserve (rule R8c); omitted = derived from the resolved --identity's own seat (machine seat) |
--stream | string | unset | stream slug the run belongs to (required) |
--track | string | unset | the TRAIN this stream belongs to — the order's track: header line, verbatim; omitted records NO DECLARATION (SQL NULL), which is a different fact from --track "", and neither is ever inferred from the stream slug |
--version-arg | string | unset | proc only: flag that prints the version string (absent = version not captured) |
--wall-clock | duration | 0s | MANDATORY wall-clock limit; on expiry the process group is SIGKILLed (e.g. 10m) |
--workdir | string | unset | absolute workdir; the child cannot write outside it (adr-0020 §2 rule) (required) |
Examples
Everything below this line is written by hand and is not regenerated.
The smallest useful run: an ordinary program under mandate
This runs a program, not an agent: no model, no brief, no laws. It exists on the record with
its own transcript, its exit status and its cost, which is the point — the same door, the
same accounting, whether the work was done by a model or by go test.
An agent session
Four things about that line are worth reading twice.
--modelis mandatory and is never a session default. The run records which source answered for it, so a model choice is always attributable.--lawsis required for an agent backend and is read fresh at every spawn, including a resume; its digest lands on the record. An unreadable or empty file refuses before anything is spawned — an empty floor is not a floor.--briefmust live inside the workdir. The session receives only its path and reads it from inside its own confinement; a brief outside is refused by name.--cost-estimate-usdis your quote, and the spend cap is derived from it rather than chosen separately.
⚠ A refusal here is a recorded outcome, not a fallback. If a precondition is missing the run refuses by name and nothing spawns — read the name rather than retrying blind.