<persisted-output>
Output too large (34.8KB). Full output saved to: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\18e1ffdb-d0cf-498f-9550-a69b8d5926d9\tool-results\bxq3h60tf.txt

Preview (first 2KB):
166 requirements: 166 complete, 0 incomplete, 0 findings
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       api prefix and adapter_name on every machinery invocation
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
  [OK] REQ-API-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       commune/signoff are file-drops, not commands
  [OK] REQ-ARCH-1  required: [impl]  stages: -doc +impl -unit -int
       Many small acyclically-layered crates
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
       Public SDK surface is spt-proto, spt-runtime, spt-msg
  [OK] REQ-ARCH-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Wire-protocol version independent of crate semver, N-1 compat window
  [OK] REQ-ARCH-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Copy-verbatim the commodity layer from the sister project
  [OK] REQ-CLI-1  required: []  stages: -doc -impl -unit -int
       spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned; bare spt endpoint = the list (M8 decisions 1-2)
  [OK] REQ-CLI-2  required: []  stages: -doc -impl -unit -int
       spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)
  [OK] REQ-CLI-3  required: []  stages: -doc -impl -unit -int
...
</persisted-output>

166 requirements: 166 complete, 0 incomplete, 0 findings
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       api prefix and adapter_name on every machinery invocation
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
  [OK] REQ-API-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       commune/signoff are file-drops, not commands
  [OK] REQ-ARCH-1  required: [impl]  stages: -doc +impl -unit -int
       Many small acyclically-layered crates
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
       Public SDK surface is spt-proto, spt-runtime, spt-msg
  [OK] REQ-ARCH-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Wire-protocol version independent of crate semver, N-1 compat window
  [OK] REQ-ARCH-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Copy-verbatim the commodity layer from the sister project
  [OK] REQ-CLI-1  required: []  stages: -doc -impl -unit -int
       spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned; bare spt endpoint = the list (M8 decisions 1-2)
  [OK] REQ-CLI-2  required: []  stages: -doc -impl -unit -int
       spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)
  [OK] REQ-CLI-3  required: []  stages: -doc -impl -unit -int
       Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)
  [OK] REQ-CONSENT-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)
  [OK] REQ-CONSENT-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)
  [OK] REQ-CONV-1  required: []  stages: -doc -impl -unit -int
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
  [OK] REQ-CONV-2  required: []  stages: -doc -impl -unit -int
       Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)
  [OK] REQ-DAEMON-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       One per-machine spt-daemon owning all per-machine state
  [OK] REQ-DAEMON-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Broker/brain split for seamless self-update
  [OK] REQ-DAEMON-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Any api invocation auto-starts the daemon if absent
  [OK] REQ-DAEMON-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Honor every KNOWN-HAZARDS invariant
  [OK] REQ-DAEMON-5  required: []  stages: -doc -impl -unit -int
       Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)
  [OK] REQ-DOCS-1  required: [doc, impl]  stages: +doc +impl -unit -int
       Dual-audience docs (human + AI dev-agent), markdown once / two depths
  [OK] REQ-DOCS-2  required: [doc, int]  stages: +doc -impl -unit +int
       Sub-10-minute runnable killer quickstart per audience
  [OK] REQ-DOCS-3  required: [doc]  stages: +doc -impl -unit -int
       Diátaxis structure; one canonical way to do X
  [OK] REQ-DOCS-4  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)
  [OK] REQ-DOCS-5  required: [impl, int]  stages: -doc +impl -unit +int
       Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked
  [OK] REQ-DOCS-6  required: [impl, unit, int]  stages: +doc +impl +unit +int
       spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)
  [OK] REQ-EP-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Day-one endpoint types; open type system
  [OK] REQ-EP-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Agent endpoints vs Shells distinction in the type model
  [OK] REQ-EP-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Messaging payloads carry typed operation commands + file blobs
  [OK] REQ-EP-4  required: [impl, unit]  stages: -doc +impl +unit -int
       PresenceChannel broker endpoint (seam day-one)
  [OK] REQ-EP-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope
  [OK] REQ-FRONT-1  required: []  stages: -doc -impl -unit -int
       Day-one launcher/manager frontend (list/launch/attach/init)
  [OK] REQ-HAZARD-CASCADE-WIPE-GUARD  required: [impl, unit]  stages: -doc +impl +unit -int
       No hard-delete of a parent hosting non-empty children (6.3)
  [OK] REQ-HAZARD-CHILD-CONSOLE-FLASH  required: [impl, unit]  stages: +doc +impl +unit -int
       Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)
  [OK] REQ-HAZARD-CONFLICT-BOTH-PRESERVED  required: [impl, unit]  stages: -doc +impl +unit -int
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
  [OK] REQ-HAZARD-CONPTY-DSR  required: [impl, unit]  stages: -doc +impl +unit +int
       ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)
  [OK] REQ-HAZARD-DAEMON-HOSTED-LIVENESS  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)
  [OK] REQ-HAZARD-DAEMON-SCHED-NONBLOCKING  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)
  [OK] REQ-HAZARD-DEFERRED-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
       Deferred spool rows excluded from the event-stream drain (1.4)
  [OK] REQ-HAZARD-DEFERRED-SURVIVE-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
       Deferred rows survive poll drain (4.4)
  [OK] REQ-HAZARD-DETACHED-PIPE-INHERIT  required: [impl, unit]  stages: +doc +impl +unit -int
       Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)
  [OK] REQ-HAZARD-DIRECT-WRITE-PRECEDENCE  required: [impl, unit]  stages: -doc +impl +unit -int
       Direct-write precedence marker (with node id) guards stale overwrite (6.5)
  [OK] REQ-HAZARD-DROP-FILE-SINGLE-WRITER  required: [impl, unit]  stages: -doc +impl +unit -int
       Drop files are daemon-owned single-writer (6.4)
  [OK] REQ-HAZARD-EBUSY-RENAME  required: [impl, unit]  stages: -doc +impl +unit -int
       tmp-write + atomic-rename + retry on Windows EBUSY (5.2)
  [OK] REQ-HAZARD-ECHO-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
       Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)
  [OK] REQ-HAZARD-ELEVATED-DAEMON-SPAWN  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       The daemon always runs unelevated in the invoking user's universe, regardless of which command spawns it: an elevated spawner de-elevates (Windows: UAC linked token via CreateProcessWithTokenW; Linux: drop to SUDO_UID/SUDO_GID + the invoker's HOME) — an elevated daemon's pipes deny unelevated clients (every later spt reads not-running→spawn→bind Access-denied) and a sudo'd daemon roots the user's state universe (5.7)
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: [impl, unit]  stages: -doc +impl +unit -int
       Envelope decode order, ampersand decoded last (4.1)
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
       Two-slice envelope parser is panic-free and tolerant (4.2)
  [OK] REQ-HAZARD-EPHEMERAL-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
       Ephemeral perch cleanup on every ring exit path (3.1)
  [OK] REQ-HAZARD-EPOCH-RESET  required: []  stages: -doc -impl -unit -int
       Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)
  [OK] REQ-HAZARD-EVENTPART-REASSEMBLY  required: [impl, unit]  stages: -doc +impl +unit -int
       EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently
  [OK] REQ-HAZARD-GEN-START-NOW  required: [impl, int]  stages: -doc +impl -unit +int
       gen_start = now() on cold-start and handoff (2.4)
  [OK] REQ-HAZARD-GRACE-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
       Grace-period wait completes before composing INIT_SIGNOFF (1.1)
  [OK] REQ-HAZARD-HANDOFF-ARGV-COMPAT  required: [impl, unit]  stages: -doc +impl +unit -int
       Broker/brain IPC + handoff argv version-tolerant (2.3)
  [OK] REQ-HAZARD-ID-CHARSET  required: [impl, unit]  stages: +doc +impl +unit -int
       Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)
  [OK] REQ-HAZARD-INBOX-NO-DOUBLE  required: [impl, unit]  stages: -doc +impl +unit -int
       No double-delivery via legacy inbox (4.5)
  [OK] REQ-HAZARD-INFO-JSON-TORN-READ  required: [impl, unit]  stages: -doc +impl +unit -int
       State-file reads tolerate concurrent writes (1.2)
  [OK] REQ-HAZARD-INSTANT-UNDERFLOW  required: [impl, unit]  stages: -doc +impl +unit -int
       Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)
  [OK] REQ-HAZARD-LOCAL-API-AUTH  required: [impl, unit]  stages: -doc +impl +unit -int
       Every local `api` mutation authenticated to an endpoint/session (codex #13)
  [OK] REQ-HAZARD-PAIR-RATE-LIMIT  required: [impl, unit]  stages: -doc +impl +unit -int
       Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)
  [OK] REQ-HAZARD-PAIR-SEED-ROTATION  required: [impl, unit]  stages: -doc +impl +unit -int
       Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)
  [OK] REQ-HAZARD-PAIR-TRANSCRIPT-BIND  required: [impl, unit]  stages: -doc +impl +unit -int
       Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)
  [OK] REQ-HAZARD-PARENT-PID-PREFER  required: []  stages: -doc -impl -unit -int
       Prefer stable parent PID / broker handle over ephemeral PID (2.1)
  [OK] REQ-HAZARD-PSYCHE-OUTBOUND-PROXY  required: [impl, unit]  stages: -doc +impl +unit -int
       Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)
  [OK] REQ-HAZARD-REGISTRY-CONCURRENT  required: [impl, unit]  stages: +doc +impl +unit -int
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
  [OK] REQ-HAZARD-REGISTRY-DIR-CREATE  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9)
  [OK] REQ-HAZARD-REGISTRY-EPOCH-LEASE  required: [impl, unit]  stages: +doc +impl +unit -int
       Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)
  [OK] REQ-HAZARD-REGISTRY-GHOST-ROWS  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       A dead node identity's registry rows must decay: only the per-(endpoint,node) epoch lease supersedes rows, so without eviction a vanished node's rows are immortal and poison bare-id resolution with phantom AcrossNodes ambiguity — evict rows whose author node has not been heard (admitted inbound feed) within the eviction window; own rows never decay; a revived node re-inserts from its durable epoch within one pump cadence (4.10)
  [OK] REQ-HAZARD-REGISTRY-STALE-CLEAN  required: [impl, unit]  stages: -doc +impl +unit -int
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-HAZARD-RESTART-IDEMPOTENT  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)
  [OK] REQ-HAZARD-SINGLE-PATH-SOURCE  required: [impl, unit]  stages: -doc +impl +unit -int
       Single path/registry source of truth; no layout ambiguity (6.1)
  [OK] REQ-HAZARD-SOFT-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
       Soft-cleanup preserves state, removes only the ready marker (6.2)
  [OK] REQ-HAZARD-STALE-INDEX-LOCK  required: [impl, unit]  stages: -doc +impl +unit -int
       Sweep stale lockfiles on daemon boot (1.3)
  [OK] REQ-HAZARD-STALE-SIGNOFF-SENTINEL  required: [impl, unit]  stages: -doc +impl +unit -int
       Stale signoff sentinel does not kill a fresh start (3.2)
  [OK] REQ-HAZARD-STDIN-SESSION-ID  required: []  stages: -doc -impl -unit -int
       Stdin session_id precedence over env (2.2)
  [OK] REQ-HAZARD-SUBPROCESS-TIMEOUT  required: [impl, unit]  stages: -doc +impl +unit -int
       Every harness/git subprocess has a timeout (5.3)
  [OK] REQ-HAZARD-SUDO-SECURE-PATH  required: [impl, unit]  stages: -doc +impl +unit -int
       Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)
  [OK] REQ-HAZARD-UNC-PATH-STRIP  required: [impl, unit]  stages: -doc +impl +unit -int
       Strip Windows UNC prefix on serialized paths (5.4)
  [OK] REQ-HAZARD-UPDATE-ROLLBACK  required: [impl, unit]  stages: -doc +impl +unit -int
       Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)
  [OK] REQ-HAZARD-WAN-ORIGIN-AUTH  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)
  [OK] REQ-HAZARD-WINDOWS-PID-RECYCLE  required: [impl, unit]  stages: -doc +impl +unit -int
       Windows PID-recycling false positives guarded (5.1)
  [OK] REQ-HAZARD-WORKER-PATH  required: [impl, unit]  stages: -doc +impl +unit -int
       Single source of truth for Worker/Psyche perch location (1.5)
  [OK] REQ-INFRA-1  required: []  stages: -doc -impl -unit -int
       GitHub issue tracking for v1; tangled.org as migration target
  [OK] REQ-INST-1  required: []  stages: -doc -impl -unit -int
       endpoint ID vs instance split (adapter-agnostic ID)
  [OK] REQ-INST-10  required: [impl, unit]  stages: -doc +impl +unit -int
       Qualified addressing [subnet:]id[@node] + ambiguity forces qualification
  [OK] REQ-INST-11  required: [impl, unit]  stages: -doc +impl +unit -int
       spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)
  [OK] REQ-INST-12  required: [impl, unit]  stages: -doc +impl +unit -int
       Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync
  [OK] REQ-INST-13  required: [impl, unit]  stages: -doc +impl +unit -int
       Subnet-exclusive sync + per-endpoint subnet-membership list
  [OK] REQ-INST-14  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated
  [OK] REQ-INST-15  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)
  [OK] REQ-INST-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-node files, synced Psyche mind
  [OK] REQ-INST-3  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Dormant (warm) / suspended (cold) resting states
  [OK] REQ-INST-4  required: [impl, unit]  stages: -doc +impl +unit -int
       active to dormant/suspended fires a transition echo commune
  [OK] REQ-INST-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-tier context sync (live to all, project to same-project)
  [OK] REQ-INST-6  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Deferred messages not delivered to dormant/suspended instances
  [OK] REQ-INST-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Subnet registry + bare-id resolution policy
  [OK] REQ-INST-8  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Remote-control mode distinct from local operation
  [OK] REQ-INST-9  required: [impl, unit]  stages: -doc +impl +unit -int
       Multi-subnet membership (same-user N subnets; cross-user seam)
  [OK] REQ-INSTALL-1  required: [doc, impl, int]  stages: +doc +impl -unit +int
       Two install paths; signed one-line script; OS-service registration
  [OK] REQ-INSTALL-2  required: [doc]  stages: +doc -impl -unit -int
       Marketplace-repackaging-friendly install
  [OK] REQ-INSTALL-3  required: [impl, int]  stages: -doc +impl -unit +int
       Idempotent + interactive-optional first run
  [OK] REQ-INSTALL-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over
  [OK] REQ-INSTALL-5  required: [impl, int]  stages: -doc +impl -unit +int
       Non-interactive install path: the canonical one-liner doubles as every adapter's pack-in on-demand install (no second mechanism); sha256-verified fetch; user-PATH registration
  [OK] REQ-INSTALL-6  required: []  stages: -doc -impl -unit -int
       Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)
  [OK] REQ-INSTALL-7  required: []  stages: -doc -impl -unit -int
       Windows inbound reachability: the elevated install leg registers the inbound-UDP firewall rule (New-NetFirewallRule); the daemon self-detects blocked inbound and renders it as the no-connection state in subnet status + the coming-online banner (covers user-scope installs that skip the elevated leg — never a silent NO_SEED_HOLDER dead-end) (M8 root cause 3)
  [OK] REQ-INSTALL-8  required: []  stages: -doc -impl -unit -int
       OS-service registration (REQ-INSTALL-1's deferred third leg): Linux systemd USER service + loginctl enable-linger (linger rides the elevated install leg; daemon starts at boot pre-login, user universe per KH 5.7, systemctl --user managed); Windows scheduled task at-logon (interactive session, no stored credentials); a node is reachable after reboot without any manual spt invocation (M8 decision 17)
  [OK] REQ-MANIFEST-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Per-adapter manifest with adapter_name and min_spt_core_version
  [OK] REQ-MIGRATE-1  required: []  stages: -doc -impl -unit -int
       Auto-detect and migrate a legacy claude_skill_owl install
  [OK] REQ-MSG-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)
  [OK] REQ-MSG-2  required: [impl, unit]  stages: -doc +impl +unit -int
       spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes
  [OK] REQ-MSG-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown
  [OK] REQ-MSG-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type="msg" from=…> otherwise, chunk oversized lines into EVENT-PART
  [OK] REQ-NET-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       WAN messaging first-class, behind default-on net feature flag
  [OK] REQ-NET-2  required: [impl]  stages: -doc +impl +unit -int
       n0 relay default + self-host knob + plain-language disclosure
  [OK] REQ-NET-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Cross-node Psyche sync over P2P replaces gh-repo-sync
  [OK] REQ-NODE-IDENTITY  required: [impl, unit]  stages: -doc +impl +unit -int
       Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex
  [OK] REQ-NOTIF-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts
  [OK] REQ-NOTIF-2  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)
  [OK] REQ-PAIR-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       TOTP-seeded SPAKE2 pairing
  [OK] REQ-PAIR-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Local trust store with TOFU + warn-on-change
  [OK] REQ-PAIR-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Fetch current pairing code from any paired node
  [OK] REQ-PAIR-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Subnet naming on first pairing
  [OK] REQ-PAIR-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing
  [OK] REQ-PAIR-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)
  [OK] REQ-PAIR-7  required: []  stages: -doc -impl -unit -int
       Subnet icon (inline image metadata, GUI-only consumer)
  [OK] REQ-PAIR-8  required: []  stages: -doc -impl -unit -int
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
  [OK] REQ-PRES-1  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Presence resolution: the presence datum (last_active_node, last_active_endpoint, ts) gossiped subnet-wide via the agent-interaction heartbeat (rides registry distribution, visibility-gated) + one first-class most-recently-active resolution API consumed by notif first-fire, update-consent delivery, consent escalation, and shell wake resolution (M5 scope decision 1: resolution only — the PresenceChannel endpoint stays deferred)
  [OK] REQ-REACH-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Off-node remote-drive detection + file transfer
  [OK] REQ-REACH-2  required: []  stages: -doc -impl -unit -int
       Remote command execution (deferred, consent-gated)
  [OK] REQ-REL-1  required: [doc, impl]  stages: +doc +impl -unit -int
       spt-releases publish-target repo: README public face, licensing split, Pages docs at the permanent lapse-proof canonical URL (ADR-0014)
  [OK] REQ-REL-2  required: [impl, int]  stages: +doc +impl -unit +int
       Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline
  [OK] REQ-REL-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)
  [OK] REQ-SEAM-ACTIVITY  required: [impl, unit]  stages: -doc +impl +unit -int
       Activity/idle reported via api sentinels, not PTY quiescence
  [OK] REQ-SEAM-CAPABILITY  required: [impl, unit]  stages: -doc +impl +unit -int
       Hostable endpoint-types capability declaration
  [OK] REQ-SEAM-HISTORY  required: [impl, unit, int]  stages: -doc +impl +unit +int
       History subsystem (fetcher / locate-normalize / native store)
  [OK] REQ-SEAM-INJECT  required: [impl, unit]  stages: -doc +impl +unit -int
       inject-input methods configurable per activity-state
  [OK] REQ-SEAM-POSTSPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
       post-spawn / api bind seam with boot nonce
  [OK] REQ-SEAM-PSYCHE  required: [impl, unit, int]  stages: -doc +impl +unit +int
       spawn-psyche seam (fresh + resume templates)
  [OK] REQ-SEAM-RESUME  required: [impl, unit]  stages: -doc +impl +unit -int
       resume-session seam (fresh-with-preload / continue-existing)
  [OK] REQ-SEAM-SPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
       spawn-session seam
  [OK] REQ-SEAM-UPDATE  required: [impl, unit]  stages: -doc +impl +unit -int
       Adapter-update avenue (file-pull / delegated command)
  [OK] REQ-SEC-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants
  [OK] REQ-SHELL-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Shell hosting machinery: shell perch under the owner (type/owner/adapter_name/status/alias), broker-launched binary + api bind local-link handshake, the three channels (command durable, text+file durable + progress-queryable, sensory REST-only never spooled + dropped-unless-owner-live), owner exclusivity (CONTEX


/grill-me over M8, then execute once open decisions are made


PowerShell

git add traceable-reqs.toml && git commit -m @'
reqs: mint M8 requirement family (rule 3, all inactive per rule 5)

15 new requirements from the ratified M8-PLAN.md:

- REQ-CLI-1..3: endpoint noun, daemon noun (+ pump-heartbeat render),
  hot-path preservation (activate at D1)
- REQ-SUBNET-5..8: attach/detach serve-state, leave/prune, re-pair trust
  overwrite (hashed OS machine id), status render honesty (activate at D2)
- REQ-INSTALL-6..8: Linux elevation install leg, Windows firewall +
  blocked-inbound render, OS-service registration (activate at D3)
- REQ-CONV-1..2, REQ-PAIR-8, REQ-DAEMON-5: addr seeding, event-driven
  advertisement, NTP TOTP offset, pump liveness/supervision (activate
  at D4; D4 also adds REQ-UPD-6 int per decision 19)
- REQ-HAZARD-EPOCH-RESET (4.11): documented hazard, stays inactive by
  decision 24

traceable-reqs check green (166 complete, 0 findings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@

Commit M8 requirement minting


[dev-freeform 1f6b7d1] reqs: mint M8 requirement family (rule 3, all inactive per rule 5)
 1 file changed, 66 insertions(+)


Updated task #2 status


Updated task #3 status


D1 starts — survey CLI structure.


54:enum Cmd {
56:    Send {
72:    Ring {
86:    Ready {
104:    Stop {
111:    Whoami,
117:    Subnet {
126:    HowTo {
134:    Digest {
149:    Rename {
161:    Access {
198:    Notify {
213:    Notif {
234:    Fork {
251:    Resources {
261:    Suspend {
271:    Wake {
279:    Shutdown {
291:    Daemon,
405:    Rename {
429:    Send {
645:                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
646:                SubnetCmd::Create { name } => cmd_subnet_create(name),
647:                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
648:                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
658:            NotifCmd::List { subnet } => cmd_notif_list(subnet),
659:            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
880:        ResourcesCmd::Set { text, id } => {
901:        ResourcesCmd::Show { id } => {
921:        ResourcesCmd::List { subnet } => {
1334:        SendOutcome::Sent => {
1342:        SendOutcome::Queued => {
1353:        SendOutcome::NoPerch => {
1394:        SendOutcome::Empty => {
1431:        RingOutcome::Replied(reply) => {
1436:        RingOutcome::TimedOut => {
1440:        RingOutcome::Queued => {
1444:        RingOutcome::Delivered => {
1451:        RingOutcome::NoPerch => {
1455:        RingOutcome::Empty => {
1567:    Subnet {
2011:            SubnetStatusRow {
2680:        AccessCmd::Allow { endpoint, node } => {
2694:        AccessCmd::Revoke { endpoint, node } => {
2717:        AccessCmd::Open { endpoint } => {
2729:        AccessCmd::List { endpoint } => {


1	//! CLI surface + dispatch for the `spt` binary (REQ-MSG-2).
2	//!
3	//! Stable arg shapes and exit codes are the contract here. Each subcommand is a
4	//! thin shim: read I/O, call the matching `spt-msg` entry point, render the
5	//! structured outcome, return an exit code. No messaging logic lives in this
6	//! layer.
7	//!
8	//! Exit codes: `0` success (delivered, queued, replied, listed) · `1` runtime
9	//! refusal (no perch, empty body, unidentifiable self) · `2` usage error
10	//! (clap, for unknown/missing args).
11	// [impl->REQ-MSG-2]
12
13	use std::io::{Read, Write};
14	use std::path::PathBuf;
15	use std::time::Duration;
16
17	use clap::{Parser, Subcommand};
18	use spt_msg::deliver::{self, SendOutcome};
19	use spt_msg::ready::ReadyAgent;
20	use spt_msg::ring::{self, RingOutcome};
21	use spt_net::net::pairing::totp::{TotpSeed, PERIOD_SECS};
22	use spt_store::perch::{self, ParentHint};
23	use spt_store::registry;
24	use spt_store::subnet::{SubnetError, SubnetStore};
25
26	use crate::elevation::{self, Elevation, Gate};
27	use crate::roster;
28
29	/// Product tag embedded in the `otpauth://` provisioning URI (the authenticator-app
30	/// issuer label). Stable user-facing string.
31	const ISSUER: &str = "spt";
32
33	/// `spt subnet show-code`/`create` refused the fetch for lack of OS elevation. Distinct
34	/// from the crate-wide codes (`0` ok · `1` runtime refusal · `2` usage): `3` says the
35	/// fetch was *refused, not failed* — re-run elevated, or read the code from your
36	/// authenticator app.
37	const EXIT_NOT_ELEVATED: i32 = 3;
38
39	#[derive(Parser)]
40	#[command(
41	    name = "spt",
42	    version,
43	    about = "spt — a harness-independent core for an agent ecosystem: inter-agent \
44	             messaging, live-agent lifecycle, terminal hosting, P2P networking, \
45	             seamless self-update. Docs: https://sabermage.github.io/spt-releases",
46	    long_about = None
47	)]
48	struct Cli {
49	    #[command(subcommand)]
50	    cmd: Cmd,
51	}
52
53	#[derive(Subcommand)]
54	enum Cmd {
55	    /// Send a message (body read from stdin); fire-and-forget.
56	    Send {
57	        /// Target perch id. May be omitted when --reply-to is given.
58	        target: Option<String>,
59	        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
60	        #[arg(long)]
61	        from: Option<String>,
62	        /// Reply to this sender; sets the target and labels output REPLIED.
63	        #[arg(long)]
64	        reply_to: Option<String>,
65	        /// Spool only (deferred / hook channel) — no live wake.
66	        #[arg(long)]
67	        deferred: bool,
68	    },
69	    /// Send and block for a reply (body read from stdin).
70	    ///
71	    /// The reply body is printed to stdout; gives up after --timeout seconds.
72	    Ring {
73	        /// Target perch id.
74	        target: String,
75	        /// Sender id (auto-detected from session if omitted).
76	        #[arg(long)]
77	        from: Option<String>,
78	        /// Seconds to wait for a reply before giving up.
79	        #[arg(long, default_value = "60")]
80	        timeout: u64,
81	    },
82	    /// Become reachable: register the perch and listen (blocks).
83	    ///
84	    /// Drains the spooled backlog first; each received message prints to
85	    /// stdout. With --once, runs a single drain+receive cycle and exits.
86	    Ready {
87	        /// This agent's perch id.
88	        id: String,
89	        /// Run a single drain+receive cycle, then exit (one-shot fallback
90	        /// for harnesses that cannot host a long-running listener).
91	        #[arg(long)]
92	        once: bool,
93	        /// Home subnet for a NEW endpoint (required on a multi-subnet
94	        /// node — home is assigned at creation, never guessed).
95	        #[arg(long)]
96	        subnet: Option<String>,
97	    },
98	    /// List this node's perches (id, state, address, ready, alive).
99	    List,
100	    /// Soft-stop a perch (spool preserved).
101	    ///
102	    /// Removes the ready marker and unregisters the perch; the spool is
103	    /// preserved.
104	    Stop {
105	        /// Perch id to stop.
106	        id: String,
107	    },
108	    /// Print this session's own perch id.
109	    ///
110	    /// Resolved from $OWL_SESSION_ID / $SPT_AGENT_ID.
111	    Whoami,
112	    /// Subnet membership: status, create, show-code.
113	    ///
114	    /// A subnet is a private group of paired machines — your agents reach
115	    /// each other across every member node. Bare `spt subnet` shows the
116	    /// membership status view.
117	    Subnet {
118	        #[command(subcommand)]
119	        action: Option<SubnetCmd>,
120	    },
121	    /// Task-oriented instructions for agents: `how-to <topic>`.
122	    ///
123	    /// The binary's own usage guidance, written for an agent to read and
124	    /// follow. Bare `how-to` lists the topics.
125	    #[command(name = "how-to")]
126	    HowTo {
127	        /// The topic to print (omit to list available topics).
128	        topic: Option<String>,
129	    },
130	    /// Show a hosted session's live activity buffer (PTY digest).
131	    ///
132	    /// The at-a-glance "what is this agent doing now" view. Pulls a
133	    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.
134	    Digest {
135	        /// The (local) endpoint id to read.
136	        id: String,
137	        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
138	        #[arg(long)]
139	        follow: bool,
140	        /// Emit structured JSON instead of the human-glanceable render.
141	        #[arg(long)]
142	        json: bool,
143	    },
144	    /// Rename an endpoint's logical id across its on-disk state.
145	    ///
146	    /// Rippled everywhere the id appears: the endpoint's perch dir, its
147	    /// nested companion/worker perches, and every record naming it.
148	    /// Refuses while the perch is live (stop it first).
149	    Rename {
150	        /// The endpoint's current (bare) id.
151	        old_id: String,
152	        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
153	        new_id: String,
154	    },
155	    /// Endpoint access whitelist for unsolicited off-node inbound.
156	    ///
157	    /// Controls which origin nodes may send an endpoint unsolicited
158	    /// off-node inbound. Absent entry = open; `allow` flips the endpoint
159	    /// to restricted; revoking the last node leaves it locked down;
160	    /// `open` deletes the restriction.
161	    Access {
162	        #[command(subcommand)]
163	        action: AccessCmd,
164	    },
165	    /// Consent grant store: gated capabilities held on this node.
166	    ///
167	    /// Default-deny (the access whitelist's opposite polarity). An
168	    /// ungranted ask escalates interactively; `add` is the durable
169	    /// allow-always answer.
170	    Grant {
171	        #[command(subcommand)]
172	        action: GrantCmd,
173	    },
174	    /// Adapter registration: what this node can drive/launch.
175	    ///
176	    /// The node-local registered set (one command for harness and shell
177	    /// adapters). Feeds creation-time adapter selection, shell discovery,
178	    /// and the self-update ripple.
179	    Adapter {
180	        #[command(subcommand)]
181	        action: AdapterCmd,
182	    },
183	    /// Shell instances: mint, list, drive, tear down owned surfaces.
184	    ///
185	    /// The driven surfaces this agent owns. `spawn` MINTS a new instance
186	    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing
187	    /// an existing offline instance back is `relink` / `persistent` /
188	    /// wake.
189	    Shell {
190	        #[command(subcommand)]
191	        action: ShellCmd,
192	    },
193	    /// Issue a subnet-wide user notification.
194	    ///
195	    /// Produced into the replicated notification spool and first-fired at
196	    /// the user's most-recently-active endpoint in that subnet. Body from
197	    /// the trailing arg, or stdin when omitted.
198	    Notify {
199	        /// Notification body (read from stdin when omitted).
200	        body: Option<String>,
201	        /// Which subnet to notify. Required only when the node holds several
202	        /// (never guessed).
203	        #[arg(long)]
204	        subnet: Option<String>,
205	        /// Issuer endpoint id (auto-detected from session if omitted).
206	        #[arg(long)]
207	        from: Option<String>,
208	    },
209	    /// Inspect and acknowledge notifications.
210	    ///
211	    /// Dismissal is the explicit ack — it latches and replicates
212	    /// subnet-wide.
213	    Notif {
214	        #[command(subcommand)]
215	        action: NotifCmd,
216	    },
217	    /// Self-update operations.
218	    ///
219	    /// `apply` is the explicit ack named by the update-consent
220	    /// notification; it re-verifies the staged release before touching
221	    /// the live daemon.
222	    Update {
223	        #[command(subcommand)]
224	        action: UpdateCmd,
225	    },
226	    /// Fork an endpoint into another subnet as a NEW identity.
227	    ///
228	    /// Home subnets are immutable — fork is the cross-subnet move, never
229	    /// a re-home. Seeds the fork with a one-time copy of the source's
230	    /// mind (live + project tiers); the two diverge immediately (no
231	    /// ongoing sync). The source is untouched unless --delete-source.
232	    /// Same-node only today (one node holds one perch per name, so a
233	    /// local fork needs a new id).
234	    Fork {
235	        /// The source endpoint (must exist on this node).
236	        src: String,
237	        /// The fork's id (must differ from the source on the same node).
238	        new_id: String,
239	        /// The fork's home subnet — the target (must be a member).
240	        #[arg(long)]
241	        subnet: String,
242	        /// Delete the source endpoint (perch + tracked mind) after the copy.
243	        #[arg(long)]
244	        delete_source: bool,
245	    },
246	    /// The subnet resource registry — an agent yellow-pages.
247	    ///
248	    /// Per-endpoint free-text service blurbs. `set` authors your own,
249	    /// `show` reads a local endpoint's, `list` projects the registry view
250	    /// over visible rows.
251	    Resources {
252	        #[command(subcommand)]
253	        action: ResourcesCmd,
254	    },
255	    /// Rest an endpoint cold (the suspend edge).
256	    ///
257	    /// The resting state machine's suspend edge. From dormant — or
258	    /// straight from active, in which case the final
259	    /// context save still fires first. Accepts a qualified `id@node` to
260	    /// suspend an instance on a paired peer.
261	    Suspend {
262	        /// The endpoint id (qualified `id@node` reaches a paired peer).
263	        id: String,
264	    },
265	    /// Wake a resting endpoint in place.
266	    ///
267	    /// Re-activates the existing seat (state's already there — no fresh
268	    /// spawn), resurfaces undismissed notifications, and requests an
269	    /// immediate context freshness pull from trusted peers. Accepts a
270	    /// qualified `id@node` for an instance on a paired peer.
271	    Wake {
272	        /// The endpoint id (qualified `id@node` reaches a paired peer).
273	        id: String,
274	    },
275	    /// Gracefully shut down an agent's own endpoint.
276	    ///
277	    /// Soft-stops the listener, then the suspend edge — the final context
278	    /// save fires and persistent shells cascade offline with it.
279	    Shutdown {
280	        /// The endpoint id (defaults to the session's own perch).
281	        id: Option<String>,
282	    },
283	    /// Harness-contract inbound surface (hook entry points).
284	    ///
285	    /// The entry points a harness's hooks fire to keep spt-core's on-disk
286	    /// state in sync.
287	    Api(crate::api::ApiArgs),
288	    /// Run the per-machine daemon. Internal: any `spt` invocation that needs
289	    /// it auto-starts it; operators rarely run it directly.
290	    #[command(hide = true)]
291	    Daemon,
292	}
293
294	/// `spt access <action>` subcommands (REQ-SEC-1).
295	#[derive(Subcommand)]
296	enum AccessCmd {
297	    /// Whitelist a node for an endpoint (creates the restriction if absent).
298	    Allow {
299	        /// The endpoint to restrict.
300	        endpoint: String,
301	        /// The origin node's pubkey hex (as shown by `spt list` / pairing).
302	        node: String,
303	    },
304	    /// Remove a node from an endpoint's whitelist. Never widens: revoking the
305	    /// last node leaves the endpoint locked down (all unsolicited refused).
306	    Revoke { endpoint: String, node: String },
307	    /// Delete an endpoint's restriction entirely — back to default-open.
308	    Open { endpoint: String },
309	    /// List restrictions (all endpoints, or one).
310	    List { endpoint: Option<String> },
311	}
312
313	/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
314	#[derive(Subcommand)]
315	enum AdapterCmd {
316	    /// Register an adapter from a local path (a dir holding `manifest.toml`,
317	    /// or the manifest file itself) or from GitHub (`--github user/repo`,
318	    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
319	    /// registers nothing. Install is the first update — the declared
320	    /// `[update]` avenue is conducted once after recording.
321	    Add {
322	        /// Local manifest source (omit when using --github).
323	        path: Option<String>,
324	        /// GitHub source `user/repo` — manifest-first, then install via the
325	        /// declared `[update]` avenue.
326	        #[arg(long)]
327	        github: Option<String>,
328	    },
329	    /// Soft-deregister: hidden from new-creation/discovery; existing and live
330	    /// instances keep running. The manifest's optional `uninstall` template
331	    /// is conducted only with --force until quiesce detection lands.
332	    Remove {
333	        name: String,
334	        /// Conduct the manifest `uninstall` template now, without waiting for
335	        /// quiesce.
336	        #[arg(long)]
337	        force: bool,
338	    },
339	    /// List registered adapters (active and soft-deregistered).
340	    List,
341	}
342
343	/// `spt grant <action>` subcommands (REQ-CONSENT-1).
344	#[derive(Subcommand)]
345	enum GrantCmd {
346	    /// Record a grant: `agent` may exercise `capability` on this node.
347	    /// Refuses the reserved deferred capability ids (remote-exec,
348	    /// instantiate-anywhere) — their gate refuses unconditionally, so a row
349	    /// would only be a footgun-in-waiting.
350	    Add {
351	        /// The gated capability id (e.g. spawn-shell, owner-shutdown).
352	        capability: String,
353	        /// The subject agent (endpoint id).
354	        agent: String,
355	        /// Narrower target within the node (e.g. the shell-adapter name for
356	        /// spawn-shell). Omitted = the node-wide row; the two never match
357	        /// each other.
358	        #[arg(long)]
359	        qualifier: Option<String>,
360	    },
361	    /// Remove the exact grant row. Never widens or narrows neighbours: only
362	    /// the named (capability, agent, qualifier) tuple goes.
363	    Revoke {
364	        capability: String,
365	        agent: String,
366	        #[arg(long)]
367	        qualifier: Option<String>,
368	    },
369	    /// List grant rows (all, or one agent's).
370	    List { agent: Option<String> },
371	}
372
373	/// `spt shell <action>` subcommands (M5-D3, REQ-EP-5). Owner-scoped: every
374	/// action operates on the invoking agent's own instances (`--owner`
375	/// overrides; auto-detected from the session otherwise).
376	#[derive(Subcommand)]
377	enum ShellCmd {
378	    /// Mint a NEW shell instance of a registered `kind="shell"` adapter:
379	    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
380	    /// starting offline (the launch + bind handshake brings it online).
381	    Spawn {
382	        /// The providing shell adapter (must be registered + active).
383	        adapter: String,
384	        /// Optional owner-unique friendly label (interchangeable with the
385	        /// canonical id for addressing; never obscures the adapter).
386	        #[arg(long)]
387	        alias: Option<String>,
388	        /// Owning endpoint id (auto-detected from session if omitted).
389	        #[arg(long)]
390	        owner: Option<String>,
391	    },
392	    /// List this owner's instances: canonical id, alias, adapter, status.
393	    List {
394	        #[arg(long)]
395	        owner: Option<String>,
396	    },
397	    /// Destroy an instance (perch removed; mint slot + alias freed).
398	    Teardown {
399	        /// Canonical id or alias.
400	        shell_ref: String,

//! CLI surface + dispatch for the `spt` binary (REQ-MSG-2).
//!
//! Stable arg shapes and exit codes are the contract here. Each subcommand is a
//! thin shim: read I/O, call the matching `spt-msg` entry point, render the
//! structured outcome, return an exit code. No messaging logic lives in this
//! layer.
//!
//! Exit codes: `0` success (delivered, queued, replied, listed) · `1` runtime
//! refusal (no perch, empty body, unidentifiable self) · `2` usage error
//! (clap, for unknown/missing args).
// [impl->REQ-MSG-2]

use std::io::{Read, Write};
use std::path::PathBuf;
use std::time::Duration;

use clap::{Parser, Subcommand};
use spt_msg::deliver::{self, SendOutcome};
use spt_msg::ready::ReadyAgent;
use spt_msg::ring::{self, RingOutcome};
use spt_net::net::pairing::totp::{TotpSeed, PERIOD_SECS};
use spt_store::perch::{self, ParentHint};
use spt_store::registry;
use spt_store::subnet::{SubnetError, SubnetStore};

use crate::elevation::{self, Elevation, Gate};
use crate::roster;

/// Product tag embedded in the `otpauth://` provisioning URI (the authenticator-app
/// issuer label). Stable user-facing string.
const ISSUER: &str = "spt";

/// `spt subnet show-code`/`create` refused the fetch for lack of OS elevation. Distinct
/// from the crate-wide codes (`0` ok · `1` runtime refusal · `2` usage): `3` says the
/// fetch was *refused, not failed* — re-run elevated, or read the code from your
/// authenticator app.
const EXIT_NOT_ELEVATED: i32 = 3;

#[derive(Parser)]
#[command(
    name = "spt",
    version,
    about = "spt — a harness-independent core for an agent ecosystem: inter-agent \
             messaging, live-agent lifecycle, terminal hosting, P2P networking, \
             seamless self-update. Docs: https://sabermage.github.io/spt-releases",
    long_about = None
)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Send a message (body read from stdin); fire-and-forget.
    Send {
        /// Target perch id. May be omitted when --reply-to is given.
        target: Option<String>,
        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
        /// Reply to this sender; sets the target and labels output REPLIED.
        #[arg(long)]
        reply_to: Option<String>,
        /// Spool only (deferred / hook channel) — no live wake.
        #[arg(long)]
        deferred: bool,
    },
    /// Send and block for a reply (body read from stdin).
    ///
    /// The reply body is printed to stdout; gives up after --timeout seconds.
    Ring {
        /// Target perch id.
        target: String,
        /// Sender id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
        /// Seconds to wait for a reply before giving up.
        #[arg(long, default_value = "60")]
        timeout: u64,
    },
    /// Become reachable: register the perch and listen (blocks).
    ///
    /// Drains the spooled backlog first; each received message prints to
    /// stdout. With --once, runs a single drain+receive cycle and exits.
    Ready {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (one-shot fallback
        /// for harnesses that cannot host a long-running listener).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (required on a multi-subnet
        /// node — home is assigned at creation, never guessed).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// List this node's perches (id, state, address, ready, alive).
    List,
    /// Soft-stop a perch (spool preserved).
    ///
    /// Removes the ready marker and unregisters the perch; the spool is
    /// preserved.
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Print this session's own perch id.
    ///
    /// Resolved from $OWL_SESSION_ID / $SPT_AGENT_ID.
    Whoami,
    /// Subnet membership: status, create, show-code.
    ///
    /// A subnet is a private group of paired machines — your agents reach
    /// each other across every member node. Bare `spt subnet` shows the
    /// membership status view.
    Subnet {
        #[command(subcommand)]
        action: Option<SubnetCmd>,
    },
    /// Task-oriented instructions for agents: `how-to <topic>`.
    ///
    /// The binary's own usage guidance, written for an agent to read and
    /// follow. Bare `how-to` lists the topics.
    #[command(name = "how-to")]
    HowTo {
        /// The topic to print (omit to list available topics).
        topic: Option<String>,
    },
    /// Show a hosted session's live activity buffer (PTY digest).
    ///
    /// The at-a-glance "what is this agent doing now" view. Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.
    Digest {
        /// The (local) endpoint id to read.
        id: String,
        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
        #[arg(long)]
        follow: bool,
        /// Emit structured JSON instead of the human-glanceable render.
        #[arg(long)]
        json: bool,
    },
    /// Rename an endpoint's logical id across its on-disk state.
    ///
    /// Rippled everywhere the id appears: the endpoint's perch dir, its
    /// nested companion/worker perches, and every record naming it.
    /// Refuses while the perch is live (stop it first).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,
    },
    /// Endpoint access whitelist for unsolicited off-node inbound.
    ///
    /// Controls which origin nodes may send an endpoint unsolicited
    /// off-node inbound. Absent entry = open; `allow` flips the endpoint
    /// to restricted; revoking the last node leaves it locked down;
    /// `open` deletes the restriction.
    Access {
        #[command(subcommand)]
        action: AccessCmd,
    },
    /// Consent grant store: gated capabilities held on this node.
    ///
    /// Default-deny (the access whitelist's opposite polarity). An
    /// ungranted ask escalates interactively; `add` is the durable
    /// allow-always answer.
    Grant {
        #[command(subcommand)]
        action: GrantCmd,
    },
    /// Adapter registration: what this node can drive/launch.
    ///
    /// The node-local registered set (one command for harness and shell
    /// adapters). Feeds creation-time adapter selection, shell discovery,
    /// and the self-update ripple.
    Adapter {
        #[command(subcommand)]
        action: AdapterCmd,
    },
    /// Shell instances: mint, list, drive, tear down owned surfaces.
    ///
    /// The driven surfaces this agent owns. `spawn` MINTS a new instance
    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing
    /// an existing offline instance back is `relink` / `persistent` /
    /// wake.
    Shell {
        #[command(subcommand)]
        action: ShellCmd,
    },
    /// Issue a subnet-wide user notification.
    ///
    /// Produced into the replicated notification spool and first-fired at
    /// the user's most-recently-active endpoint in that subnet. Body from
    /// the trailing arg, or stdin when omitted.
    Notify {
        /// Notification body (read from stdin when omitted).
        body: Option<String>,
        /// Which subnet to notify. Required only when the node holds several
        /// (never guessed).
        #[arg(long)]
        subnet: Option<String>,
        /// Issuer endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
    },
    /// Inspect and acknowledge notifications.
    ///
    /// Dismissal is the explicit ack — it latches and replicates
    /// subnet-wide.
    Notif {
        #[command(subcommand)]
        action: NotifCmd,
    },
    /// Self-update operations.
    ///
    /// `apply` is the explicit ack named by the update-consent
    /// notification; it re-verifies the staged release before touching
    /// the live daemon.
    Update {
        #[command(subcommand)]
        action: UpdateCmd,
    },
    /// Fork an endpoint into another subnet as a NEW identity.
    ///
    /// Home subnets are immutable — fork is the cross-subnet move, never
    /// a re-home. Seeds the fork with a one-time copy of the source's
    /// mind (live + project tiers); the two diverge immediately (no
    /// ongoing sync). The source is untouched unless --delete-source.
    /// Same-node only today (one node holds one perch per name, so a
    /// local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// The subnet resource registry — an agent yellow-pages.
    ///
    /// Per-endpoint free-text service blurbs. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {
        #[command(subcommand)]
        action: ResourcesCmd,
    },
    /// Rest an endpoint cold (the suspend edge).
    ///
    /// The resting state machine's suspend edge. From dormant — or
    /// straight from active, in which case the final
    /// context save still fires first. Accepts a qualified `id@node` to
    /// suspend an instance on a paired peer.
    Suspend {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Wake a resting endpoint in place.
    ///
    /// Re-activates the existing seat (state's already there — no fresh
    /// spawn), resurfaces undismissed notifications, and requests an
    /// immediate context freshness pull from trusted peers. Accepts a
    /// qualified `id@node` for an instance on a paired peer.
    Wake {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint.
    ///
    /// Soft-stops the listener, then the suspend edge — the final context
    /// save fires and persistent shells cascade offline with it.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Harness-contract inbound surface (hook entry points).
    ///
    /// The entry points a harness's hooks fire to keep spt-core's on-disk
    /// state in sync.
    Api(crate::api::ApiArgs),
    /// Run the per-machine daemon. Internal: any `spt` invocation that needs
    /// it auto-starts it; operators rarely run it directly.
    #[command(hide = true)]
    Daemon,
}

/// `spt access <action>` subcommands (REQ-SEC-1).
#[derive(Subcommand)]
enum AccessCmd {
    /// Whitelist a node for an endpoint (creates the restriction if absent).
    Allow {
        /// The endpoint to restrict.
        endpoint: String,
        /// The origin node's pubkey hex (as shown by `spt list` / pairing).
        node: String,
    },
    /// Remove a node from an endpoint's whitelist. Never widens: revoking the
    /// last node leaves the endpoint locked down (all unsolicited refused).
    Revoke { endpoint: String, node: String },
    /// Delete an endpoint's restriction entirely — back to default-open.
    Open { endpoint: String },
    /// List restrictions (all endpoints, or one).
    List { endpoint: Option<String> },
}

/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
#[derive(Subcommand)]
enum AdapterCmd {
    /// Register an adapter from a local path (a dir holding `manifest.toml`,
    /// or the manifest file itself) or from GitHub (`--github user/repo`,
    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
    /// registers nothing. Install is the first update — the declared
    /// `[update]` avenue is conducted once after recording.
    Add {
        /// Local manifest source (omit when using --github).
        path: Option<String>,
        /// GitHub source `user/repo` — manifest-first, then install via the
        /// declared `[update]` avenue.
        #[arg(long)]
        github: Option<String>,
    },
    /// Soft-deregister: hidden from new-creation/discovery; existing and live
    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until quiesce detection lands.
    Remove {
        name: String,
        /// Conduct the manifest `uninstall` template now, without waiting for
        /// quiesce.
        #[arg(long)]
        force: bool,
    },
    /// List registered adapters (active and soft-deregistered).
    List,
}

/// `spt grant <action>` subcommands (REQ-CONSENT-1).
#[derive(Subcommand)]
enum GrantCmd {
    /// Record a grant: `agent` may exercise `capability` on this node.
    /// Refuses the reserved deferred capability ids (remote-exec,
    /// instantiate-anywhere) — their gate refuses unconditionally, so a row
    /// would only be a footgun-in-waiting.
    Add {
        /// The gated capability id (e.g. spawn-shell, owner-shutdown).
        capability: String,
        /// The subject agent (endpoint id).
        agent: String,
        /// Narrower target within the node (e.g. the shell-adapter name for
        /// spawn-shell). Omitted = the node-wide row; the two never match
        /// each other.
        #[arg(long)]
        qualifier: Option<String>,
    },
    /// Remove the exact grant row. Never widens or narrows neighbours: only
    /// the named (capability, agent, qualifier) tuple goes.
    Revoke {
        capability: String,
        agent: String,
        #[arg(long)]
        qualifier: Option<String>,
    },
    /// List grant rows (all, or one agent's).
    List { agent: Option<String> },
}

/// `spt shell <action>` subcommands (M5-D3, REQ-EP-5). Owner-scoped: every
/// action operates on the invoking agent's own instances (`--owner`
/// overrides; auto-detected from the session otherwise).
#[derive(Subcommand)]
enum ShellCmd {
    /// Mint a NEW shell instance of a registered `kind="shell"` adapter:
    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
    /// starting offline (the launch + bind handshake brings it online).
    Spawn {
        /// The providing shell adapter (must be registered + active).
        adapter: String,
        /// Optional owner-unique friendly label (interchangeable with the
        /// canonical id for addressing; never obscures the adapter).
        #[arg(long)]
        alias: Option<String>,
        /// Owning endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        owner: Option<String>,
    },
    /// List this owner's instances: canonical id, alias, adapter, status.
    List {
        #[arg(long)]
        owner: Option<String>,
    },
    /// Destroy an instance (perch removed; mint slot + alias freed).
    Teardown {
        /// Canonical id or alias.
        shell_ref: String,


400	        shell_ref: String,
401	        #[arg(long)]
402	        owner: Option<String>,
403	    },
404	    /// Set/replace an instance's alias (owner-unique).
405	    Rename {
406	        /// Canonical id or current alias.
407	        shell_ref: String,
408	        /// The new alias.
409	        alias: String,
410	        #[arg(long)]
411	        owner: Option<String>,
412	    },
413	    /// Drive the shell with a typed capability command (the durable command
414	    /// channel): the op + positional args are vocabulary-checked against the
415	    /// manifest's `[shell.capabilities]`, spooled on the shell perch, and
416	    /// drained by the manifest's `command_receipt` mode (relay / stdin).
417	    Cmd {
418	        /// Canonical id or alias.
419	        shell_ref: String,
420	        /// The capability op + args (vocabulary-checked against the manifest).
421	        #[arg(trailing_var_arg = true)]
422	        op: Vec<String>,
423	        #[arg(long)]
424	        owner: Option<String>,
425	    },
426	    /// Send a text and/or file payload down the durable 2-way text+file
427	    /// channel (agent→shell; the shell answers via ordinary `spt send`).
428	    /// File transfers are progress-queryable by xfer id.
429	    Send {
430	        /// Canonical id or alias.
431	        shell_ref: String,
432	        /// The text payload.
433	        text: Option<String>,
434	        /// A file to transfer to the shell.
435	        #[arg(long)]
436	        file: Option<PathBuf>,
437	        #[arg(long)]
438	        owner: Option<String>,
439	    },
440	    /// Bring an existing offline (persistent) instance back online: re-spawns
441	    /// the binary with a fresh link token; the perch onlines at its bind.
442	    Relink {
443	        /// Canonical id or alias.
444	        shell_ref: String,
445	        #[arg(long)]
446	        owner: Option<String>,
447	    },
448	}
449
450	/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
451	#[derive(Subcommand)]
452	enum ResourcesCmd {
453	    /// Author this endpoint's blurb (the agent refines its own at runtime;
454	    /// an empty string clears it back to the node-config seed).
455	    Set {
456	        /// The blurb text ("" clears).
457	        text: String,
458	        /// Which local endpoint (auto-detected from the session if omitted).
459	        #[arg(long)]
460	        id: Option<String>,
461	    },
462	    /// Show a local endpoint's authored blurb.
463	    Show {
464	        /// The local endpoint id (auto-detected if omitted).
465	        id: Option<String>,
466	    },
467	    /// The subnet resource-registry projection: `(id, node, status, blurb)`
468	    /// over VISIBLE rows only (hidden endpoints never appear — discovery
469	    /// leaks nothing a viewer couldn't reach).
470	    List {
471	        /// Limit to one subnet (all member subnets otherwise).
472	        #[arg(long)]
473	        subnet: Option<String>,
474	    },
475	}
476
477	/// `spt notif <action>` subcommands (ADR-0007).
478	#[derive(Subcommand)]
479	enum NotifCmd {
480	    /// List notifications (all member subnets, or one).
481	    List {
482	        /// Limit to one subnet.
483	        #[arg(long)]
484	        subnet: Option<String>,
485	    },
486	    /// Dismiss (ack) a notification by id — latches, replicates subnet-wide.
487	    Dismiss {
488	        /// The notif id (as shown by `spt notif list`).
489	        notif_id: String,
490	    },
491	}
492
493	/// `spt update <action>` subcommands (REQ-UPD-4).
494	#[derive(Subcommand)]
495	enum UpdateCmd {
496	    /// Apply the staged, verified self-update now.
497	    Apply,
498	}
499
500	/// `spt subnet <action>` subcommands (REQ-SUBNET-1). Noun namespace with
501	/// subcommand verbs and positional subnet names — never `--subnet` flags
502	/// (M7 plan decision 3).
503	#[derive(Subcommand)]
504	enum SubnetCmd {
505	    /// Show subnet membership: name, paired nodes, endpoints.
506	    ///
507	    /// Never prints seeds, epochs, or pairing codes. Bare `spt subnet` is
508	    /// the same view.
509	    Status {
510	        /// Limit to one subnet (all member subnets otherwise).
511	        name: Option<String>,
512	        /// Per-node rows: label, online/offline, [online endpoints/total].
513	        #[arg(long)]
514	        nodes: bool,
515	    },
516	    /// Mint a fresh subnet and print its joining material.
517	    ///
518	    /// This node becomes the sole seed-holder. Prints the current 6-digit
519	    /// code, the `otpauth://` provisioning URI, and a terminal QR of it.
520	    /// Gated behind OS elevation (the seed-reveal path).
521	    Create {
522	        /// The new subnet's name.
523	        name: String,
524	    },
525	    /// Show a subnet's current 6-digit pairing code (+ URI and QR).
526	    ///
527	    /// The re-provisioning surface: prints the same joining material as
528	    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.
529	    /// Gated behind OS elevation (or read the code from your authenticator
530	    /// app). With no name the node's sole subnet is used; if it holds
531	    /// several, the name is required (never guessed).
532	    ShowCode {
533	        /// Which subnet's code to show. Required only when the node holds several.
534	        name: Option<String>,
535	    },
536	    /// Pair this machine into an existing subnet (guided).
537	    ///
538	    /// Finds a member machine over LAN + relay rendezvous and runs the
539	    /// code-authenticated pairing ceremony against it. Prompts for the
540	    /// name and code when omitted (interactive terminals). Gated behind
541	    /// OS elevation — joining enrolls this whole machine.
542	    Join {
543	        /// The subnet to join (as named on the member machine).
544	        name: Option<String>,
545	        /// The current 6-digit code (`spt subnet show-code` on a member
546	        /// machine, or your authenticator app).
547	        #[arg(long)]
548	        code: Option<String>,
549	    },
550	}
551
552	/// Top-level commands an agent is meant to run from its own session; every
553	/// other visible command lands in the operator-facing "User commands" group.
554	/// Grouping is render-time only (clap has no native subcommand help
555	/// headings); `help_groups_cover_every_command` keeps this set honest.
556	const AGENT_COMMANDS: &[&str] = &[
557	    "api",
558	    "how-to",
559	    "notify",
560	    "ready",
561	    "resources",
562	    "ring",
563	    "send",
564	    "shell",
565	    "shutdown",
566	    "whoami",
567	];
568
569	/// Build the top-level help template: the default clap layout, with the flat
570	/// `Commands:` block replaced by two alphabetized groups (user-facing first,
571	/// agent-facing second). Entries are generated from the derived `Command`
572	/// itself, so abouts can never drift from the list.
573	fn grouped_help_template(cmd: &mut clap::Command) -> String {
574	    cmd.build(); // materialize the implicit `help` subcommand
575	    let (mut user, mut agent) = (Vec::new(), Vec::new());
576	    for sc in cmd.get_subcommands() {
577	        if sc.is_hide_set() {
578	            continue;
579	        }
580	        let name = sc.get_name().to_string();
581	        let about = sc.get_about().map(ToString::to_string).unwrap_or_default();
582	        let group = if AGENT_COMMANDS.contains(&name.as_str()) {
583	            &mut agent
584	        } else {
585	            &mut user
586	        };
587	        group.push((name, about));
588	    }
589	    user.sort();
590	    agent.sort();
591	    // One aligned name column across both groups; abouts are one-liners by
592	    // contract (see the unit test), so no entry ever wraps.
593	    let pad = user
594	        .iter()
595	        .chain(&agent)
596	        .map(|(n, _)| n.len())
597	        .max()
598	        .unwrap_or(0);
599	    let render = |rows: &[(String, String)]| {
600	        rows.iter()
601	            .map(|(n, a)| format!("  {n:<pad$}  {a}\n"))
602	            .collect::<String>()
603	    };
604	    format!(
605	        "{{before-help}}{{about-with-newline}}\n{{usage-heading}} {{usage}}\n\n\
606	         User commands:\n{}\n\
607	         Agent commands:\n{}\n\
608	         Options:\n{{options}}{{after-help}}",
609	        render(&user),
610	        render(&agent),
611	    )
612	}
613
614	/// Parse args and dispatch. Returns the process exit code.
615	pub fn run() -> i32 {
616	    use clap::{CommandFactory, FromArgMatches};
617	    let mut cmd = Cli::command();
618	    let template = grouped_help_template(&mut cmd);
619	    let matches = cmd.help_template(template).get_matches();
620	    let cli = match Cli::from_arg_matches(&matches) {
621	        Ok(cli) => cli,
622	        Err(e) => e.exit(),
623	    };
624	    match cli.cmd {
625	        Cmd::Send {
626	            target,
627	            from,
628	            reply_to,
629	            deferred,
630	        } => cmd_send(target, from, reply_to, deferred),
631	        Cmd::Ring {
632	            target,
633	            from,
634	            timeout,
635	        } => cmd_ring(target, from, timeout),
636	        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
637	        Cmd::List => cmd_list(),
638	        Cmd::Stop { id } => cmd_stop(&id),
639	        Cmd::Whoami => cmd_whoami(),
640	        Cmd::Subnet { action } => {
641	            match action.unwrap_or(SubnetCmd::Status {
642	                name: None,
643	                nodes: false,
644	            }) {
645	                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
646	                SubnetCmd::Create { name } => cmd_subnet_create(name),
647	                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
648	                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
649	            }
650	        }
651	        Cmd::HowTo { topic } => cmd_how_to(topic.as_deref()),
652	        Cmd::Access { action } => cmd_access(action),
653	        Cmd::Grant { action } => cmd_grant(action),
654	        Cmd::Adapter { action } => cmd_adapter(action),
655	        Cmd::Shell { action } => cmd_shell(action),
656	        Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),
657	        Cmd::Notif { action } => match action {
658	            NotifCmd::List { subnet } => cmd_notif_list(subnet),
659	            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
660	        },
661	        Cmd::Update { action } => match action {
662	            UpdateCmd::Apply => cmd_update_apply(),
663	        },
664	        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
665	        Cmd::Fork {
666	            src,
667	            new_id,
668	            subnet,
669	            delete_source,
670	        } => cmd_fork(&src, &new_id, &subnet, delete_source),
671	        Cmd::Resources { action } => cmd_resources(action),
672	        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
673	        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
674	        Cmd::Shutdown { id } => cmd_shutdown(id),
675	        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
676	        Cmd::Api(args) => crate::api::run(args),
677	        Cmd::Daemon => cmd_daemon(),
678	    }
679	}
680
681	/// `spt digest <id>` — show a daemon-hosted session's live activity buffer
682	/// (REQ-TERM-4 / ADR-0008). Ensures the daemon is up, then pulls a snapshot or
683	/// follows the delta-stream over the digest-control channel. Local addressing
684	/// only: a qualified `[subnet:]id@node` form is refused (cross-node is M4).
685	// [impl->REQ-TERM-4]
686	fn cmd_digest(id: &str, follow: bool, json: bool) -> i32 {
687	    // Local addressing only — the qualified cross-node form is M4 (spt-net).
688	    if id.contains('@') || id.contains(':') {
689	        eprintln!(
690	            "CROSS_NODE_M4:{id} — qualified [subnet:]id@node addressing is M4, not yet supported"
691	        );
692	        return 1;
693	    }
694	    // The digest lives in the daemon; bring it up if absent (best-effort —
695	    // a start failure still tries the socket; announced when it transitions).
696	    let _ = ensure_daemon_announced();
697	    let name = spt_daemon::digest_socket_name();
698
699	    if follow {
700	        let res = spt_daemon::follow(&name, id, 0, |up| {
701	            if json {
702	                println!("{}", spt_daemon::update_to_json(up));
703	            } else {
704	                print!("{}", spt_daemon::render_update(up));
705	            }
706	            let _ = std::io::stdout().flush();
707	            true // stream until the connection ends / the user interrupts
708	        });
709	        return match res {
710	            Ok(()) => 0,
711	            Err(e) => {
712	                eprintln!("DIGEST_FOLLOW_FAIL:{id}: {e}");
713	                1
714	            }
715	        };
716	    }
717
718	    match spt_daemon::pull_snapshot(&name, id) {
719	        Ok(Some((digest, version))) => {
720	            if json {
721	                println!("{}", spt_daemon::digest_to_json(&digest));
722	            } else {
723	                print!("{}", spt_daemon::render_digest(&digest));
724	                let _ = std::io::stdout().flush();
725	            }
726	            eprintln!("DIGEST:{id} version={version}");
727	            0
728	        }
729	        Ok(None) => {

        shell_ref: String,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Set/replace an instance's alias (owner-unique).
    Rename {
        /// Canonical id or current alias.
        shell_ref: String,
        /// The new alias.
        alias: String,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Drive the shell with a typed capability command (the durable command
    /// channel): the op + positional args are vocabulary-checked against the
    /// manifest's `[shell.capabilities]`, spooled on the shell perch, and
    /// drained by the manifest's `command_receipt` mode (relay / stdin).
    Cmd {
        /// Canonical id or alias.
        shell_ref: String,
        /// The capability op + args (vocabulary-checked against the manifest).
        #[arg(trailing_var_arg = true)]
        op: Vec<String>,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Send a text and/or file payload down the durable 2-way text+file
    /// channel (agent→shell; the shell answers via ordinary `spt send`).
    /// File transfers are progress-queryable by xfer id.
    Send {
        /// Canonical id or alias.
        shell_ref: String,
        /// The text payload.
        text: Option<String>,
        /// A file to transfer to the shell.
        #[arg(long)]
        file: Option<PathBuf>,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Bring an existing offline (persistent) instance back online: re-spawns
    /// the binary with a fresh link token; the perch onlines at its bind.
    Relink {
        /// Canonical id or alias.
        shell_ref: String,
        #[arg(long)]
        owner: Option<String>,
    },
}

/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
#[derive(Subcommand)]
enum ResourcesCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb.
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
    /// The subnet resource-registry projection: `(id, node, status, blurb)`
    /// over VISIBLE rows only (hidden endpoints never appear — discovery
    /// leaks nothing a viewer couldn't reach).
    List {
        /// Limit to one subnet (all member subnets otherwise).
        #[arg(long)]
        subnet: Option<String>,
    },
}

/// `spt notif <action>` subcommands (ADR-0007).
#[derive(Subcommand)]
enum NotifCmd {
    /// List notifications (all member subnets, or one).
    List {
        /// Limit to one subnet.
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Dismiss (ack) a notification by id — latches, replicates subnet-wide.
    Dismiss {
        /// The notif id (as shown by `spt notif list`).
        notif_id: String,
    },
}

/// `spt update <action>` subcommands (REQ-UPD-4).
#[derive(Subcommand)]
enum UpdateCmd {
    /// Apply the staged, verified self-update now.
    Apply,
}

/// `spt subnet <action>` subcommands (REQ-SUBNET-1). Noun namespace with
/// subcommand verbs and positional subnet names — never `--subnet` flags
/// (M7 plan decision 3).
#[derive(Subcommand)]
enum SubnetCmd {
    /// Show subnet membership: name, paired nodes, endpoints.
    ///
    /// Never prints seeds, epochs, or pairing codes. Bare `spt subnet` is
    /// the same view.
    Status {
        /// Limit to one subnet (all member subnets otherwise).
        name: Option<String>,
        /// Per-node rows: label, online/offline, [online endpoints/total].
        #[arg(long)]
        nodes: bool,
    },
    /// Mint a fresh subnet and print its joining material.
    ///
    /// This node becomes the sole seed-holder. Prints the current 6-digit
    /// code, the `otpauth://` provisioning URI, and a terminal QR of it.
    /// Gated behind OS elevation (the seed-reveal path).
    Create {
        /// The new subnet's name.
        name: String,
    },
    /// Show a subnet's current 6-digit pairing code (+ URI and QR).
    ///
    /// The re-provisioning surface: prints the same joining material as
    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.
    /// Gated behind OS elevation (or read the code from your authenticator
    /// app). With no name the node's sole subnet is used; if it holds
    /// several, the name is required (never guessed).
    ShowCode {
        /// Which subnet's code to show. Required only when the node holds several.
        name: Option<String>,
    },
    /// Pair this machine into an existing subnet (guided).
    ///
    /// Finds a member machine over LAN + relay rendezvous and runs the
    /// code-authenticated pairing ceremony against it. Prompts for the
    /// name and code when omitted (interactive terminals). Gated behind
    /// OS elevation — joining enrolls this whole machine.
    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },
}

/// Top-level commands an agent is meant to run from its own session; every
/// other visible command lands in the operator-facing "User commands" group.
/// Grouping is render-time only (clap has no native subcommand help
/// headings); `help_groups_cover_every_command` keeps this set honest.
const AGENT_COMMANDS: &[&str] = &[
    "api",
    "how-to",
    "notify",
    "ready",
    "resources",
    "ring",
    "send",
    "shell",
    "shutdown",
    "whoami",
];

/// Build the top-level help template: the default clap layout, with the flat
/// `Commands:` block replaced by two alphabetized groups (user-facing first,
/// agent-facing second). Entries are generated from the derived `Command`
/// itself, so abouts can never drift from the list.
fn grouped_help_template(cmd: &mut clap::Command) -> String {
    cmd.build(); // materialize the implicit `help` subcommand
    let (mut user, mut agent) = (Vec::new(), Vec::new());
    for sc in cmd.get_subcommands() {
        if sc.is_hide_set() {
            continue;
        }
        let name = sc.get_name().to_string();
        let about = sc.get_about().map(ToString::to_string).unwrap_or_default();
        let group = if AGENT_COMMANDS.contains(&name.as_str()) {
            &mut agent
        } else {
            &mut user
        };
        group.push((name, about));
    }
    user.sort();
    agent.sort();
    // One aligned name column across both groups; abouts are one-liners by
    // contract (see the unit test), so no entry ever wraps.
    let pad = user
        .iter()
        .chain(&agent)
        .map(|(n, _)| n.len())
        .max()
        .unwrap_or(0);
    let render = |rows: &[(String, String)]| {
        rows.iter()
            .map(|(n, a)| format!("  {n:<pad$}  {a}\n"))
            .collect::<String>()
    };
    format!(
        "{{before-help}}{{about-with-newline}}\n{{usage-heading}} {{usage}}\n\n\
         User commands:\n{}\n\
         Agent commands:\n{}\n\
         Options:\n{{options}}{{after-help}}",
        render(&user),
        render(&agent),
    )
}

/// Parse args and dispatch. Returns the process exit code.
pub fn run() -> i32 {
    use clap::{CommandFactory, FromArgMatches};
    let mut cmd = Cli::command();
    let template = grouped_help_template(&mut cmd);
    let matches = cmd.help_template(template).get_matches();
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(cli) => cli,
        Err(e) => e.exit(),
    };
    match cli.cmd {
        Cmd::Send {
            target,
            from,
            reply_to,
            deferred,
        } => cmd_send(target, from, reply_to, deferred),
        Cmd::Ring {
            target,
            from,
            timeout,
        } => cmd_ring(target, from, timeout),
        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
        Cmd::List => cmd_list(),
        Cmd::Stop { id } => cmd_stop(&id),
        Cmd::Whoami => cmd_whoami(),
        Cmd::Subnet { action } => {
            match action.unwrap_or(SubnetCmd::Status {
                name: None,
                nodes: false,
            }) {
                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
            }
        }
        Cmd::HowTo { topic } => cmd_how_to(topic.as_deref()),
        Cmd::Access { action } => cmd_access(action),
        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),
        Cmd::Shell { action } => cmd_shell(action),
        Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),
        Cmd::Notif { action } => match action {
            NotifCmd::List { subnet } => cmd_notif_list(subnet),
            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
        },
        Cmd::Update { action } => match action {
            UpdateCmd::Apply => cmd_update_apply(),
        },
        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Fork {
            src,
            new_id,
            subnet,
            delete_source,
        } => cmd_fork(&src, &new_id, &subnet, delete_source),
        Cmd::Resources { action } => cmd_resources(action),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
        Cmd::Shutdown { id } => cmd_shutdown(id),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
        Cmd::Api(args) => crate::api::run(args),
        Cmd::Daemon => cmd_daemon(),
    }
}

/// `spt digest <id>` — show a daemon-hosted session's live activity buffer
/// (REQ-TERM-4 / ADR-0008). Ensures the daemon is up, then pulls a snapshot or
/// follows the delta-stream over the digest-control channel. Local addressing
/// only: a qualified `[subnet:]id@node` form is refused (cross-node is M4).
// [impl->REQ-TERM-4]
fn cmd_digest(id: &str, follow: bool, json: bool) -> i32 {
    // Local addressing only — the qualified cross-node form is M4 (spt-net).
    if id.contains('@') || id.contains(':') {
        eprintln!(
            "CROSS_NODE_M4:{id} — qualified [subnet:]id@node addressing is M4, not yet supported"
        );
        return 1;
    }
    // The digest lives in the daemon; bring it up if absent (best-effort —
    // a start failure still tries the socket; announced when it transitions).
    let _ = ensure_daemon_announced();
    let name = spt_daemon::digest_socket_name();

    if follow {
        let res = spt_daemon::follow(&name, id, 0, |up| {
            if json {
                println!("{}", spt_daemon::update_to_json(up));
            } else {
                print!("{}", spt_daemon::render_update(up));
            }
            let _ = std::io::stdout().flush();
            true // stream until the connection ends / the user interrupts
        });
        return match res {
            Ok(()) => 0,
            Err(e) => {
                eprintln!("DIGEST_FOLLOW_FAIL:{id}: {e}");
                1
            }
        };
    }

    match spt_daemon::pull_snapshot(&name, id) {
        Ok(Some((digest, version))) => {
            if json {
                println!("{}", spt_daemon::digest_to_json(&digest));
            } else {
                print!("{}", spt_daemon::render_digest(&digest));
                let _ = std::io::stdout().flush();
            }
            eprintln!("DIGEST:{id} version={version}");
            0
        }
        Ok(None) => {


Structure clear. Before executing — check daemon control surface (stop/status machinery existence) for the `daemon` noun.


crates\spt\src\cli.rs:1064:fn cmd_daemon() -> i32 {


crates\spt-daemon\src\daemon.rs:231:pub fn is_running() -> bool {
crates\spt-daemon\src\seedmap.rs:206:pub fn ping(name: &str) -> io::Result<()> {