# U3 — USHER lifecycle verb surface (releases#5)

The lane that retires `spt endpoint run` and lands the ratified lifecycle verbs. This
file is the lane's working record: what was ratified, what it costs, and the design
lines that were decided rather than discovered. The DECISION is the grilled verb table
in releases#5 (bag grill, 2026-08-04) — cite that, not this file, for authority.

## The ratified surface

| verb | shape | notes |
|---|---|---|
| `endpoint create <new-id>` | `[--subnet S] [--adapter A] [--cwd DIR]` | the ONLY mint. Subnet immutable after creation. Records adapter + cwd (cwd defaults to cwd-at-create) as the endpoint's initial session defaults. |
| `endpoint start <id>` | `[--adapter A] [--cwd DIR]` | positional id, no `--id`. A NEW session on the endpoint's most-recent adapter in its most-recent project folder — NEVER the env cwd. An UNKNOWN id REFUSES, pointing at `create` (a typo must not mint a phantom endpoint). `--adapter`/`--cwd` override AND become the new remembered defaults. |
| `endpoint resume <id>` | — | the endpoint's LATEST session. |
| `endpoint auto-start <id>` | `[--off]` | replaces `run --save` as a standalone lever. |
| `go <id>` | — | top-level. The operator's "take me to this endpoint". |
| `endpoint run` | RETIRED | clean break: parse error + pointer, no shim. |

`wake` / `start` / `resume` each name the other two in their `--help`.

`go`'s ladder: online+uncontrolled → rc · online+controlled → interactive "kick to take
control y/n" (rc `--take` behind the confirm) · suspended → wake then rc · offline WITH
sessions → resume then rc · offline WITHOUT sessions → MINT the first session then rc
(operator-ruled) · engine-room → defers entirely to the gated `rc engine-room` path.

## Arg budget (IR-33) — ruled, not discovered

The retired `run` carried 9 leaf args. The ratified surface is the MIN spelling and
totals 11: create 4, start 3, resume 1, auto-start 2, go 1. **Net +2.** No
attach/view trio anywhere — the attach flow stays `rc` / `go`; `go` takes no `--yes`
because its kick confirm is interactive by design (scripts and agents use `rc --take`).

The −9 is only real because `run` retires through a RAW-ARGV PRE-SCAN ahead of
`Cli::command()` (the U1 precedent), which keeps the retired args out of the derive
tree entirely and gives the refusal text full control. A hidden clap variant would have
kept all 9 resident and put the lane at +11..+18, outside IR-33's measured ~6 margin at
both ends.

Commit 1 removes the ceiling rather than budgeting under it — see below.

## CAPABILITY DELTA (release-notes flag)

**Specific-session resume retires with `run` and has no U3 replacement.** Today's
`endpoint run --resume <session>` can name one particular prior session; the ratified
`endpoint resume <id>` is LATEST-ONLY. This is a deliberate narrowing, not an
oversight. If the field wants specific-session resume back it arrives as a new request,
never as a quiet flag.

**A fresh bringup is now TWO commands.** `endpoint create <id>` MINTS ONLY — the
skeleton perch, the permanent home, and the recorded adapter/cwd defaults — and
does not start a session; `endpoint start <id>` (or `spt go <id>`, which also
attaches) brings the first one up. So the one-shot
`endpoint run --adapter A --id I --start` becomes `create` + `start`.
Nothing is LOST, so this is not a narrowing — but it is a shape change to every
script that ever brought an endpoint up, and it belongs in the notes beside the
resume narrowing.

The fork was decided here rather than found in the table (todlando, ratified by
doyle 2026-08-04): the table says create "records adapter + cwd as the endpoint's
INITIAL SESSION DEFAULTS", a phrase that only earns its keep if a LATER verb reads
them — and, decisively, `go`'s ladder carries a first-class "offline WITHOUT
sessions → MINT the first session" rung, which is near-unreachable unless
zero-session endpoints are routine. A `create` that also launched would be the
same overloading this lane exists to delete.

## Design lines decided before build

**`go`'s offline discriminant reads the SESSION LEDGER, not the record's `session_id`.**
After a clean `endpoint stop` the record's `session_id` PERSISTS BY DESIGN — it is the
CAS identity anchor the racy death observers key on (terminal_normalize /
converge_dead_relay / the resume-unbound stamp, `spt-store` info.rs) and is never a
liveness claim; `status` alone answers liveness. So a non-empty `session_id` means only
"a session existed here once", and a ladder that read it would make the
offline-WITHOUT-sessions arm reachable only on a never-bound endpoint. The ladder
instead asks the ledger (`<perch>/sessions.log`, `spt-store::sessions`) for a
NON-PROVISIONAL row — the same anchor `resolve_resume_session` already uses, because
ledger rows are hook-authored and a non-provisional row IS a real harness session. A
spawn provisional is not a resumable session and must not satisfy the with-sessions arm.

**The picker keeps its door and grows no new one.** The picker's entry is bare `spt` on
an interactive terminal (`decide_bare` → `picker::run(None, None)`), untouched by this
lane — that is what satisfies the operator's hard constraint. `run`'s two prefill
quadrants (`--adapter`-only pre-select, `--id`-only create-new prefill) were argv
conveniences OF THE RETIRING VERB and retire with it: the id-only case's replacement is
already ratified (`start <unknown-id>` refuses pointing at `create`), and the
adapter-only case dies with its verb. No new `pick` verb — the lane does not grow
surface the table did not ratify. A quadrant that ceases to exist is DELETED with its
reason recorded here, never silently dropped.

**Stale on-disk shortcuts get a remedy, not a parse error.** Generated launchers
(`spt-<id>.cmd` / `spt-<id>`) already written into operator project dirs bake a
`spt endpoint run --adapter … --id …` argv. The new generated body emits `spt go <id>`
— the launcher's intent is "take me to this endpoint whatever its state", which is
`go`'s ladder verbatim, and a launcher opens a console so the interactive kick confirm
has its TTY. The pre-scan refusal names BOTH the new verb AND shortcut regeneration
(the picker's `s` keybind), and the shortcut SENTINEL is bumped so a stale launcher is
detectable rather than merely broken.

## Build order

1. **Commit 1 — the stack ceiling (IR-33), deleted rather than budgeted.** `main`
   calls `cli::run()` on the main thread, whose stack on Windows is the PE-header
   default (1 MiB); the clap tree's construction cost scales with the argument
   population and had come within ~6 args of exhausting it. The CLI moves onto a
   `thread::Builder` with an explicit large stack. Conditions: the before/after
   overflow point is MEASURED by probe (dummy hidden args, bisected, `--version` as
   the invocation — the one that dies first because the overflow happens during tree
   construction); exit-code and panic fidelity are preserved THROUGH the join and
   toothed, not asserted in a comment; and the debug `--help` smoke rides regardless,
   because it also backstops IR-32's trigger.
2. **T1–T5, all green on today's surface, each mutation-proved from a committed
   baseline** — see below.
3. **The rename**, then the string sweep, then the docs regen.

## Lane status (2026-08-04)

Landed and pushed on `build/usher-u3-verb-surface`:

- `ea9f43b` — commit 1, the stack ceiling (REQ-CLI-STACK-HEADROOM, T5 rode it).
- `631e96b` — T1, the route table. Mutation-proved twice (`--view` renamed out of
  the derive tree; `--create`'s `conflicts_with = "resume"` deleted), both red
  with their exact face, baseline restored green.
- `c8ca788` — T2, the wildcard-free event-loop arm. Proved by probe variant WITH
  its negative control: new arm ⇒ 2×E0004 (event_loop + dispatch), old wildcard
  ⇒ 1×E0004 (dispatch only, event_loop swallows it).

T3 (`resolve_run_target_quadrants`) already existed and is green — it is a CARRY
at the rename, not new work. `REQ-USHER-LIFECYCLE-VERBS` is registered in
`traceable-reqs.toml` with `required_stages = []`; the stages activate on the
rename commit that delivers the verbs.

Recorded for the rename, from T1's mutation run: a REFUSAL row cannot witness a
lost door. Under the `--view`→`--watch` mutation the exclusion test still passed,
because `--attach --view` refuses either way once `--view` stops existing. The
doors live in the positive table; the refusals only carry the exclusions.

Then build-order item 3, in three commits:

- `a966891` — **the rename**. The ratified verbs, `go`'s ladder as a pure
  function, the raw-argv pre-scan retiring `endpoint run`, T4 with its
  unknown-subcommand negative control, T1 carried through (every retired door
  is now a REFUSAL row, not a silent absence), the launcher body → `spt go
  <id>` with the sentinel bumped to v2, `auto-start` as its own verb with its
  own reserved-id refusal, the CONTEXT.md glossary amendment, the docs regen,
  and `REQ-USHER-LIFECYCLE-VERBS` activated at `[doc, impl, unit]`.
  `resolve_run_target` and T3 were DELETED with the picker prefill quadrants
  they served; what survives of that resolution is `pick_adapter`, which
  carries REQ-RUN-ID-REUSES-ADAPTER forward.
- `8dbc19a` — **the e2e estate**. All 22 argv invocations re-spelled, with each
  test's SUBJECT re-established where the retirement removed a shape (see the
  commit body). `tests/common::create_endpoint` is the shared mint half.
- **the prose sweep** (this commit).

Measured on the lane: `cargo clippy --workspace --all-targets` clean at zero
warnings; `cargo test -p spt --bins` 605 green; `traceable-reqs check` exit 0;
`xtask check` (docs-drift) OK.

- `d53b65c` — **the estate's findings**, from the full-estate window doyle
  granted (2026-08-04). See below.

## Full-estate result (window granted by doyle, 2026-08-04)

**16/16 binaries, 31 tests, ONE clean pass at `d53b65c`, zero skipped.** The
window earned its cost: it found a defect no unit could have, and a miss of
mine.

- **`go`'s wake rung was wrong, twice.** `rest_state == "suspended"` does NOT
  mean an operator rested it — `terminal_normalize` stamps `status=offline` +
  `rest_state=suspended` on ANY session death, so a killed harness is
  indistinguishable on disk from a suspended endpoint. The rung fired on
  ordinary death and ended at `rc`, which re-asks liveness of the persisted row
  and fast-fails offline. A discriminant that does not discriminate, plus a
  terminal rung where an additive one belonged. Now: wake → bring up → attach,
  and the live rungs attach SESSION-CONFIRMED rather than re-asking a worse
  authority than the honest probe that reached them.
- **Two rig specimens moved, not dropped.** The dummy binds with the
  spt-MINTED id, so every ledger row it writes is a spawn provisional — not a
  resumable session. Latest-only `resume` correctly degraded to fresh, leaving
  `resume_template` with no resume to select and `run_no_dup`'s B4 arriving as
  a CREATE intent. Both seed the harness-reported row a real harness writes at
  bind.
- **The runner's own identity reds the estate.** This session is a live agent,
  so its env carries `OWL_SESSION_ID` and `daemon stop` refuses
  (`DAEMON_STOP_REFUSED`). The estate runner must strip the agent env family
  before any gate run from a perched session.
- **A miss of mine:** `wake_resume_bind_e2e` still spelled `endpoint run` — a
  17th site. The sweep grep was `head -40`-truncated and I read it as the
  population. Re-swept untruncated: nine survivors repo-wide, all deliberate.

## Teeth

- **T1** — a `Cli::try_parse_from` route table over every picker-entry and direct-run
  invocation. Written against today's `run` spellings and green; the rename then edits
  ONLY the argv literals, so a door lost to the rename goes red on a line whose sole
  change was spelling. (`cli.rs` has exactly one `try_parse_from` today — there is
  effectively no parse-level estate to inherit.)
- **T2** — the picker event loop's `done => return Ok(done)` catch-all becomes an
  exhaustive match, so the `Outcome` enum is pinned by the COMPILER at both prod sites
  (`dispatch` is already wildcard-free). A hand-built walk in a test would stay
  trivially true when a variant is added; only a wildcard-free match at the prod sites
  compiles red.
- **T3** — the 5-quadrant `resolve_run_target` unit carried onto whatever entry
  resolution survives.
- **T4** — the retirement refusal: `endpoint run …` exits the parse-error code AND its
  pointer names the new verb, with an ordinary unknown subcommand as the negative
  control so a generic clap error cannot pass as the pointer.
- **T5** — a debug-build smoke: `spt --help` answers non-empty on stdout at exit 0.

## Sweep scope

Live contracts only: `docs-site` (10 sites), `docs/` (28), `traceable-reqs.toml` prose
(48), plus a `CONTEXT.md` glossary amendment for the lifecycle verb surface (cite the
grill comment; no new ADR unless the lane hits a fork the table does not answer). The
ADRs (12 sites) and the root JIT/planning docs (110) are HISTORICAL RECORD and stay as
written. The shipped runtime strings are 12 sites across 6 files, two of them the
generated launcher body; two existing test asserts pin two of those strings and change
with them. The cross-repo `/sptc:` skill-doc sweep is named and stays OUT of this lane.

The schedule driver is not the strings: 22 real argv invocations live across 14
real-daemon e2e files, the slowest estate in the repo.
