258 requirements: 258 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-API-4 required: [doc, impl, unit] stages: +doc +impl +unit -int api resolves the adapter manifest (+ profile + install dir) from `--adapter name:profile` via the registry when `--manifest` is omitted; `--manifest` becomes an optional OVERRIDE (unregistered / local-dev manifests). Removes the require-both-flags redundancy — a registered adapter's live bringup / digest / capability needs only `--adapter` — and yields the precise install dir (the record's source_dir) rather than the --manifest parent, closing the copy-mode psyche-binary edge (v0.8.0) [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: [impl, unit] 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 ] grouped by subnet with SELF pinned, --detail adding the ex-resources yellow-pages blurb projection; bare spt endpoint = the list (M8 decisions 1-2, 25) [OK] REQ-CLI-2 required: [impl, unit] 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: [impl, unit] 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-CLI-4 required: [] stages: -doc +impl -unit -int User-facing CLI output is human-readable: DIRECT-USER commands (e.g. adapter update/list/use) render friendly prose instead of raw CODE:RESULT markers — "claude-spt is up to date (0.2.0)." not "ADAPTER_UPDATE_UPTODATE:claude-spt: installed 0.2.0, latest 0.2.0". Strictly bounded to the direct-user surface: the adapter-PARSED bringup tokens (SEEDED/BOUND/READY/NO_SEED on seed/listen, which adapters grep) stay machine-parseable — humanization is additive (a human line beside the marker, or a --porcelain/--quiet split), never a silent rename of a dual-contract marker. The user-facing bringup composition belongs to the adapter (perri); this REQ owns only the direct-user CLI surface. (v0.9.0) [OK] REQ-CLI-HELP-MARKDOWN required: [impl, unit] stages: -doc +impl +unit -int `spt --help` (and every subcommand --help) renders the inline Markdown authored in the clap doc-comments as terminal styling, never as literal markers: `**bold**` → ANSI bold, `` `code` `` → ANSI cyan, `[text](url)` → `text`. The markers are STRIPPED either way — a raw `**` or backtick must NEVER reach the user (the operator-reported v0.12.0 defect: help text reads `**ctrl-b**` and stray backticks verbatim). Color/bold escapes are emitted ONLY when the help is going to a real terminal AND color is not suppressed (NO_COLOR unset · CLICOLOR != 0 · CLICOLOR_FORCE forces on); a pipe / redirect / CI / NO_COLOR falls back to strip-only (clean plaintext, zero escapes) so machine-readable help is byte-identical regardless of marker syntax. Pure transform over the clap-rendered help string at the single run()/bare_invocation chokepoint; preserves pre-existing ANSI (CSI sequences passed through untouched), never spans markers across a newline, leaves unmatched/empty markers literal, and does not alter the help layout. (v0.12.1) [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-CONSENT-3 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill). [OK] REQ-CONV-1 required: [impl, unit] 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: [impl, unit] 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: [impl, unit] 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-DAEMON-6 required: [impl, unit] stages: -doc +impl +unit -int Service-aware `daemon start`/`stop`: when an OS service manager has a registered spt-daemon for this user, `spt daemon start` and `spt daemon stop` drive THAT service (so stop doesn't IPC-kill a unit that auto-restart-fights for the broker socket — the kitsubito 2026-06-08 loop). `start` graduates from a `run` alias to a first-class background verb (ensure-up, idempotent, non-blocking); stop routes managed→manager, manual→IPC. Linux=systemd user unit (`systemctl --user start|stop|is-active spt-daemon`, detected by unit-file presence); Windows=no controllable manager (the logon task is boot-only), so start=detached spawn / stop=IPC. [OK] REQ-DAEMON-7 required: [impl, unit] stages: -doc +impl +unit -int `daemon run` is foreground-consistent on every platform: the invoking process IS the daemon, blocks until signalled, never auto-detaches or respawns into an invisible background task. The detached/de-elevated background behavior lives ONLY in `start`. Windows: an ELEVATED `daemon run` refuses with guidance (use `start`, or an unelevated shell) instead of respawning detached/de-elevated and vanishing (KH 5.7 preserved — it still never serves elevated). [OK] REQ-DAEMON-8 required: [impl, unit] stages: -doc +impl +unit -int Internal auto-start prefers the service: `ensure_running` (any spt command's implicit daemon start, REQ-DAEMON-3) routes through the service-aware start path — when a manager has a registered service it starts THAT, never a competing manual `spawn_detached` daemon that would fight the service for the socket. [OK] REQ-DAEMON-9 required: [impl, unit] stages: -doc +impl +unit -int Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders. [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 : in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it) [OK] REQ-ELEVATE-1 required: [doc, impl, unit] stages: +doc +impl +unit -int Cross-platform self-elevating re-launch for privilege-gated commands: a pure decision seam `decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath{AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint}` selecting how to re-acquire privilege, and the per-OS impure launchers it dispatches — Windows UAC console (ShellExecuteW `runas` on the abs-exe + verbatim argv; the elevated child does the work, prints 'You can close this window', and pauses for a keypress; the original prints 'Elevated terminal launched…' and exits 0; NEVER pipes the child's stdout back across the privilege boundary), Linux desktop pkexec (preferred, native polkit GUI auth) else x-terminal-emulator -e sudo (fallback list x-terminal-emulator→gnome-terminal→konsole→xterm), the existing interactive-TTY inline sudo, and the headless/no-path floor that prints the absolute-path command. Reused by every gated command (not subnet-specific). Generalizes should_auto_elevate. [OK] REQ-ENDPOINT-LIST-MERGE-LOCAL required: [doc, impl, unit] stages: +doc +impl +unit -int `spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1) [OK] REQ-ENDPOINT-PURGE required: [doc, impl, unit, int] stages: +doc +impl +unit +int `spt endpoint purge ` fully removes an endpoint AND every record keyed on it — the formal teardown devs/CI need for clean test setup/reset. NOT consent-gated (a local dev/test op — no peer consent). OFFLINE-ONLY: refuses while the endpoint is online / daemon-hosted (deleting records out from under a live host risks the daemon re-creating or re-hosting mid-purge); `--force` STOPS it first (endpoint stop → wait for the daemon reconcile to un-host + reap the Psyche) THEN purges. Confirms interactively unless `--yes` (the CI path). Refuses purging the CALLER's OWN running id. All LOCAL — purge reaches only THIS node's records; a remote endpoint's records can't be touched, and its subnet-registry rows decay via the epoch-lease eviction (REQ-HAZARD-REGISTRY-DECAY). Removes: (1) the perch dir TREE recursively — owlery// incl every nested {id}-psyche / {id}-w* / shells child (info.json, ready marker, sessions.log ledger, spool.db, inbox, .idle/.more-done sentinels, auth token); (2) the registry address (registry::unregister_address); (3) the context store — ContextStore::remove_endpoint(id): the a- branch+worktree + the / rows from every p- branch (the same fn `fork --delete-source` already uses); (4) node-local trust rows keyed on the id — access.json + visibility.json. Reuse-heavy: it is `fork --delete-source` generalized (recursive perch remove + unregister + remove_endpoint) + the trust-record cleanup; `endpoint rename` already enumerates the same record set + uses the same offline-only gate. (v0.12.0) [OK] REQ-ENDPOINT-STOP-OFFLINE required: [impl, unit] stages: -doc +impl +unit -int H3: `spt endpoint stop ` marks the endpoint OFFLINE (alive=false), not merely de-readied. cmd_stop (cli.rs:2994-3010) removes the ready marker + unregisters the address but does NOT set status offline, so a stopped daemon-hosted endpoint still reports alive=true (status=online latch). FIX: add set_status(perch, STATUS_OFFLINE) to cmd_stop — folds with B2 (same setter). Unit: stop → is_perch_alive=false / alive=false. (v0.12.0) [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-EP-6 required: [doc, impl, unit] stages: +doc +impl +unit +int Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6 [OK] REQ-EP-7 required: [doc, impl, unit] stages: +doc +impl +unit -int Durable live-role.md: a per-agent broad-purpose statement in tracked/agents// beside live-context.md (replicates with the mind on the same a- branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite ` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing) [OK] REQ-EP-8 required: [] stages: +doc -impl -unit -int AlwaysOnEndpoint: a resident, addressable, mindless endpoint whose adapter binary the daemon supervises continuously — register-triggered by an adapter-option's `[always-on]` manifest section, one supervised binary per `[:profile]`, running independent of agent liveness. It self-manages its `#`-addressed channel endpoints via the existing `api bind` (one connection fronts many). The SECOND class of spt-core-boot-launched third-party binary (after the shell wake-watcher); supervision reuses the wake-watcher scaffolding (backoff / give-up latch / one-per-instance lock / orphan-kill / brain-side reconcile) MINUS the offline-only flip — always online, never resting (no dormant/suspended states). Two-way: agents message it; it may call `endpoint wake `, target-side authorized (REQ-INST-3/6 wake resolution + access whitelist + shell_wake_spawn_anywhere — no caller-ownership gate). First consumer downstream: spt-discord. [OK] REQ-EP-9 required: [] stages: +doc -impl -unit -int `#` always-on address sigil: a reserved LEADING sigil marking an AlwaysOnEndpoint, extending the REQ-INST-10 grammar to `[subnet:]#id[@node]`. Mandatory + bijective — `#name` ⟺ always-on endpoint, bare `name` ⟺ agent endpoint — so the router resolves endpoint class from the address alone, before any registry lookup. Sits ABOVE REQ-HAZARD-ID-CHARSET: the address parser strips the single leading `#` before id validation, so the bare/stored id stays charset-clean and a mid-id `#` remains rejected (the charset contract is unchanged). [OK] REQ-FRONT-1 required: [] stages: -doc -impl -unit -int Day-one launcher/manager frontend (list/launch/attach/init) [OK] REQ-HAZARD-ATTACH-WEDGE required: [int] stages: -doc -impl -unit +int A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock. DISPOSITION = PROVE-DON'T-CHANGE (doyle GATE-PASS @e883f45, 2026-06-18): this ROOT is the SUPERSEDED v0.12.0 hypothesis — the post-L0 code ALREADY prevents the wedge, so NO fail-fast / worker-count code was added. serve_attach forwards fire-and-forget (net_stream_send op_id=None) and the broker-side send_stream is already BROKER-QUIC-DEADLINE-bounded (bounded_block_on, 10s); the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (RecvHalf::Loopback, retentive_cap==0 → evict-not-park) so a dead rc (a dropped IPC subscriber) never backs peer_w up; bounded_block_on parks the BROKER DISPATCH thread, not a net worker → no worker-pool exhaustion (full mechanism in the required_stages comment). Folds the status=online sub-check: a dead spt-hosted endpoint is marked OFFLINE within one reconcile tick on abrupt child death (broker exit-waiter reaps the session → B2 sees it absent) — PROVEN, no change. (v0.12.1) [OK] REQ-HAZARD-BRAIN-RESPAWN-PATH required: [doc, impl, unit, int] stages: +doc +impl +unit +int The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11. [OK] REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE required: [] stages: -doc -impl -unit -int B4 (deepest): a bare brain restart (broker survives) REHYDRATES the live-agent lifecycle so post-restart endpoints are hosted + attachable. Today resume_sessions (brainproc.rs:186, brain.rs:797-809) re-subscribes to the broker's PTY sessions but ALL BrainLifecycle instances (lifecycle.rs:58-130; the ephemeral brain.rs:254-275) are LOST on restart → a post-restart live endpoint gets no livehost → its Psyche is never (re)hosted and new spawns die / can't attach until a FULL daemon reset (operator: perri's brain kill+restart wedged everything until a full daemon kill). FIX: on brain startup, rebuild a BrainLifecycle per resumed live-capable session — load the manifest from the adapter registry → instantiate → start the pulse — the rehydrate the resume no-op cannot do. Composes with B2 (the reconcile re-hosts from the honest on-disk status after rehydrate). (v0.12.0) [OK] REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP required: [impl, unit, int] stages: -doc +impl +unit +int A bare brain restart leaves EXACTLY ONE `{id}-psyche` process per endpoint — no duplicate. On an abrupt brain death stop_host never runs (the LiveSet + owned child handles die with the brain) and Breap's job/group only reaps at DAEMON stop, so the PRIOR brain's Psyche stays ALIVE; the respawned brain's reconcile re-hosts a SECOND Psyche and overwrites the `{id}-psyche` perch pid, leaving the old one untracked + alive = a duplicate that lingers until daemon-stop (the operator's 'brain kill+restart wedged everything'). FIX: at brain start, BEFORE the first reconcile re-hosts, reap any pre-existing `{id}-psyche` orphan — ID-SPECIFICALLY (recycle-safe on the shared box, where sibling agents share the `claude` basename): scoped-kill the recorded pid ONLY IF it is alive AND its exe basename == the adapter's psyche program (normalize_basename) AND its COMMAND LINE contains the full psyche id `-psyche` (baked via {id}); a sibling never carries THIS id, and any unreadable signal FAILS SAFE (decline to reap — a missed dup is bounded by Breap, a wrong-kill is catastrophic). CAVEAT: the cmdline carries `-psyche` only when the adapter's psyche_init.command uses {id} (the norm); a non-{id} adapter safely MISSES the reap (today's behavior, Breap bounds it) — never a wrong-kill. (v0.12.0) [OK] REQ-HAZARD-BROKER-PROCESS-ISOLATION required: [doc, impl, unit, int] stages: +doc +impl +unit +int Broker and brain are separate processes: the broker runs as its own long-lived per-machine process that survives every brain restart, so a routine (brain-only) self-update restarts the brain onto the swapped binary while every hosted endpoint (PTY child, live QUIC conn, listening socket) stays untouched at the PROCESS level. The in-process-thread broker (daemon.rs:165-170) is a regression that silently unrealizes REQ-UPD-3 — apply degrades to an in-process Brain::handoff no-op and new code does not run until an unrelated restart (KNOWN-HAZARDS 6.7). Evidence must prove process-level survival (SPIKE-01/03 productionized as int: PTY child + live QUIC survive a brain-PROCESS restart onto a swapped binary), re-pointing the regression-masked in-process int tags currently on REQ-DAEMON-2 / REQ-UPD-3 (ADR-0018). [OK] REQ-HAZARD-BROKER-QUIC-DEADLINE required: [doc, impl, unit, int] stages: +doc +impl +unit +int The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16. [OK] REQ-HAZARD-BROKER-SEED-WIRE-SKEW required: [doc, impl, unit] stages: +doc +impl +unit -int A daemon-state wire-format change (e.g. the v0.9.0 adapter-agnostic Seed) does NOT take effect until a DELIBERATE full broker restart: the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004 no-terminate-during-update forbids auto-killing it), so a NEW-version CLI talking to a still-resident OLD broker fails the seed handshake — the old broker cannot deserialize the new Seed (its formerly-required `adapter` field is gone) and drops the conn without an ack, which surfaces to the CLI as a raw UnexpectedEof 'failed to fill whole buffer'. spt-core must (a) surface an ACTIONABLE diagnostic on that seed-ack EOF (name the stale-broker cause + the `spt daemon stop` fix — the broker restarts on the next api call), never the cryptic io error; and (b) document the operational rule (a deliberate broker restart is required on any daemon-state wire change — NOT automatic) + the FORWARD discipline (daemon-state/Seed schema changes stay additive + serde-default so a resident OLD broker tolerates a NEW CLI across a brain-only update; note this would NOT have rescued 0.9.0 itself, since the old broker's `adapter` was a required field). perri PREP-4 FINDING 1 (v0.9.0 CLI vs stale 0.8.x broker). [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-DAEMON-STOP-BARRIER required: [impl, unit] stages: -doc +impl +unit -int B3: `spt daemon stop` then an immediate `spt daemon start` does NOT race — stop fully completes before it returns. Today request_stop (seedmap.rs:240-255) returns on the KIND_STOPPING ack (sent seedmap.rs:174-176) BEFORE the seed socket unbinds, so a following is_running ping (daemon.rs:375) wins the exit window and start reports ALREADY_RUNNING (operator: daemon stop → STOPPED then start → ALREADY_RUNNING). FIX: unbind/stop-gate the seed socket BEFORE acking KIND_STOPPING, OR request_stop waits for a ping-to-fail before returning. Unit: stop then immediate is_running()==false. (v0.12.0) [OK] REQ-HAZARD-DAEMON-STOP-REAP required: [impl, unit] stages: -doc +impl +unit -int Breap: `spt daemon stop` REAPS the spt-hosted children it spawned — no orphaned psyche/harness processes. Today a stop leaves ~8 orphaned claude-spt-psyche.exe + spt.exe: Psyches are spawned DETACHED (runtime.rs:342-356, the Child is dropped — 'Detached' ~349) and the livehost stop flag Arc is NEVER raised (brainproc.rs:227-230 holds it 'for symmetry'). FIX: on stop, raise the livehost stop flag AND kill the spawned psyche/spt-hosted children — via a Windows job object / Unix process-group so the children die with the daemon (not detached-immortal). Folds with B3 (both the stop path). (v0.12.0) [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-MANIFEST required: [impl, unit] stages: -doc +impl +unit -int A pointer-mode (delegated / GhReleaseManaged) adapter whose binary/manifest is not yet extracted is reported with a CLEAR diagnostic, never silently dropped. Today such an adapter reads its manifest LIVE from source_dir (registry.rs manifest_dir ~146/149); a deferred / un-extracted install makes load_manifest fail → registered() (~410, filter_map(.ok())) SILENTLY DROPS the row → downstream ADAPTER_UNRESOLVED + a cryptic os-error-2 on `spt adapter use`. FIX: surface a clear diagnostic at the resolver + at `adapter use` (name the adapter + the deferred/missing-manifest cause + the fix), not a silent filter-drop and not a bare os-error-2; consider an eager manifest copy at register time so host_binaries survive before the binary download completes. doyle Finding A. (post-v0.10.0) [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-ENDPOINT-RUN-ATTACH-OUTPUT required: [impl, unit, int] stages: -doc +impl +unit +int A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1) [OK] REQ-HAZARD-ENV-SUBST required: [impl, unit, int] stages: -doc +impl +unit +int `spt endpoint run` HONORS manifest [env.] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0) [OK] REQ-HAZARD-ENVELOPE-CR-LINESAFE required: [impl, unit] stages: -doc +impl +unit -int Envelope CR-linesafety (4.1): the line-framed EVENT codec must neutralize raw carriage returns — `event_body_escape` folds CRLF/lone-CR to the codec's representable linebreak (`\n`→`
`) BEFORE framing, so a body carrying `\r` (Windows `echo`/CRLF text crossing nodes) cannot survive into the single-line envelope and trigger a receiver terminal CR→col0 overwrite that corrupts the frame. Robustness on unrepresentable input, NOT a wire-format change (decoder untouched, amp-last invariant held). Belt-and-suspenders: `spt send`/`ring` also trim stdin (parity with `notify`). [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-HOSTED-LIVENESS-RECONCILE required: [impl, unit, int] stages: -doc +impl +unit +int B2 KEYSTONE: a daemon-hosted (spt-hosted) endpoint's info.json status is RECONCILED to real liveness, not left latched online. The broker exit-waiter (broker.rs:889-910) reaps its in-mem session table + emits ExitEvent but NEVER touches info.json; lifecycle::mark_offline only fires on Psyche teardown — so a dead/exited harness (operator closed the tab) stays status=online forever (is_perch_alive returns ONLINE for daemon-hosted, liveness.rs:80-93). FIX (doyle ruled PULL-PRIMARY — the live-status analog of REQ-HAZARD-ROSTER-GHOST): the livehost reconcile loop (reconcile_once livehost.rs:226-313) queries the broker's live session set (KIND_SESSIONS) each tick and, for any status=online live_agent perch PAST the boot grace whose endpoint has NO live broker session, marks it offline (lifecycle::mark_offline → status=offline → is_perch_alive=false). GATED on spt-hosted (controllable==Some(true)) so a HARNESS-HOSTED relay live agent (api listen, legitimately online with no broker session) is NEVER mis-marked. Crash-robust + self-healing on the next tick (clear-on-event is not crash-robust alone). PUSH (brain ExitEvent→mark_offline) is an OPTIONAL fast-path only if the daemon brain is reliably subscribed to all hosted sessions; correctness rides the pull. Broker stays stateless (ADR-0004 §B — brain owns the info.json write). (v0.12.0) [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=None gated on forward duration_since only (5.9) [OK] REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE required: [impl, unit, int] stages: -doc +impl +unit +int B5: `spt daemon start` does NOT revive phantom Psyches for dead-but-online-latched perches. Today reconcile_once (livehost.rs:285) spawns a Psyche per status=online live_agent perch at boot WITHOUT verifying the harness child / {id}-psyche is actually alive — so a Cold start after an unclean stop revives N psyches for N dead-but-latched perches (3 psyches for 3 dead perches). FIX: gate the boot psyche-spawn on real child-liveness — a perch with NO live broker session (the B2 reconcile signal) is marked OFFLINE at boot instead of hosted, so a dead-harness perch is never revived. Shares the B2 reconcile loop (this is its boot-gate arm); composes with B2's honest latch. Also closes wall-a's psyche_host_error gap (residency-confirm does not run at boot tick-1, livehost.rs:395-441 / 257-263). (v0.12.0) [OK] REQ-HAZARD-LIVEHOST-BOOT-RACE required: [impl, unit, int] stages: -doc +impl +unit +int The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging. [OK] REQ-HAZARD-LIVEHOST-NONRESIDENT required: [impl, unit, int] stages: -doc +impl +unit +int A daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure, surfaced like a spawn failure (closes the v0.8.1 residual masking): the REQ-HAZARD-LIVEHOST-BOOT-RACE signal stamps `psyche_host_error` only when `spawn_psyche` returns Err, NOT when the detached spawn() returns Ok but the child dies within moments (e.g. a bad-argv child exiting 2 — the F-009 case). That leaves the residual 'online + no Psyche + no cause' gap: the nested `{id}-psyche` info.json is written status=online with a real-but-DEAD pid and the PARENT perch carries NO psyche_host_error (perri's F-010: tasklist showed 0 host procs across the window while info.json read online). The host MUST confirm RESIDENCY — a hosted child not alive (or whose `{id}-psyche` perch never re-registers / has a dead pid) within N seconds of spawn is treated as a host failure: stamp the parent perch `psyche_host_error{reason:"host not resident within s (psyche perch missing/dead pid)"}` (and do not leave a phantom online nested perch). Closes the last masking gap the v0.8.1 fix left open. perri's F-010 (v0.8.1 dogfood). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE. [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-PUMP-IPC-DEADLINE required: [doc, impl, unit] stages: +doc +impl +unit -int The single-threaded peer pump's brain-IPC reads are deadline-bounded (PUMP_PEER_IO_TIMEOUT, total-wait per call); a TimedOut read POISONS the client and escalates to a SUPERVISED RESTART, never a per-peer retry — a black-holed peer must never wedge the whole pump [OK] REQ-HAZARD-RC-ATTACH-FAILFAST required: [impl, unit, int] stages: -doc +impl +unit +int B1: `spt rc ` to a DEAD or non-streaming session fails fast with a clear message, never an INFINITE blank screen. Today rc.rs run_attach (209-231) + pump spawns PUMP_IPC_READER and blocks: the poll times out each slice but the stream never produces output, so the operator sees a permanent blank (operator: fresh wall-f attached, closed tab, then `spt rc wall-f` HUNG — the broker still resolved a session for it). FIX: (a) once B2 lands, gate attach on is_online/status — an offline endpoint yields a clean 'endpoint offline, start it' not an attach; (b) fail-fast — if the attach-open ack / first output does not arrive within a bound, surface a clear message, never an infinite blank; (c) the broker EOFs the attach stream when the session's child is dead, so rc's existing PumpEnd::BrokerGone graceful path (REQ-HAZARD-RC-EOF) catches it. PIN the exact sub-mechanism with a repro test FIRST (dead-session-lingers-in-broker vs reaped-but-rc-waits vs alive-resting-no-wake — the wall-f Windows tab-close: child alive-silent vs dead-not-reaped). (v0.12.0) [OK] REQ-HAZARD-RC-EOF required: [impl, unit] stages: -doc +impl +unit -int A severed broker stream during a live rc session surfaces GRACEFULLY, never as a raw io error that crashes the PTY. The rc read-loop (rc.rs:352-362) continues only on WouldBlock/TimedOut; ANY other read_event_until error — including UnexpectedEof 'failed to fill whole buffer' — returns Err → RC_FAIL → the PTY 'crashes' from the user's view. Confirmed trigger: a deliberate `spt daemon stop` (broker bounce) severs an active rc (perri stopped the daemon to release owlery watch handles). Same severed-broker-stream EOF class as the v0.9.1 seed fix (seed_fail_message) and the listener-death case — spt-core must classify a broker-gone EOF and (a) surface a CLEAR actionable message ('daemon stopped/restarted — re-run / reconnect'), never the raw buffer error, and ideally (b) AUTO-REATTACH to the same session on the fresh broker (the broker is the daemon-lifetime anchor; it returns on the next `spt api` call). FOLD two side-observations: (1) `spt daemon stop` SILENTLY drops active rc/live sessions — warn ('N active session(s) will drop') or graceful-detach on stop; (2) the daemon holds owlery WATCH HANDLES on perch dirs so a torn-down perch dir stays 'Device busy' until a full daemon stop releases them (perri's rt-* cleanup) — a torn-down perch's handle should release without a daemon stop. doyle Finding C, root-caused. (post-v0.10.0) [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-ROLLBACK-STATE-COMPAT required: [doc, impl, unit] stages: +doc +impl +unit -int A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists; unmintable retroactively once a migration ships. [OK] REQ-HAZARD-ROSTER-GHOST required: [impl, unit] stages: -doc +impl +unit -int A LOCAL subnet roster entry whose backing perch is erased does NOT keep advertising Active (no phantom perch-less endpoint). `api session-end --erase` removes the perch (owlery dir gone) but the subnet roster (identity/registry/.json) keeps the endpoint's instance row ACTIVE with no backing perch; `endpoint stop` says 'address unregistered' yet the line persists; no CLI verb forgets a roster entry, and a hand-edit is re-added by the single-writer daemon advertiser. FIX: daemon-side self-heal — the advertiser DROPS/forgets a LOCAL roster entry whose backing perch no longer exists (stops advertising it Active), and/or a `forget`/evict verb; verify whether the epoch lease eventually evicts it (slow-self-heal) vs a real leak and scope accordingly. doyle secondary finding (perri). (post-v0.10.0) [OK] REQ-HAZARD-SELF-ELEVATE required: [unit] stages: -doc +impl +unit -int Self-elevation (REQ-ELEVATE-1) re-runs the EXACT original invocation with the binary's ABSOLUTE exe path — never widening privilege scope, never adding/altering args, never via a PATH-resolved bare name, never via a shell-interpolated command string (argv-array only, no `sh -c`); the elevated child drops state back to the user (composes with the 5.7 de-elevation) and NEVER re-elevates (loop-safe: decide_elevation_path returns AlreadyElevated whenever the process is already Elevated, on every OS). The user's UAC/polkit/sudo prompt is the only consent gate — we never bypass it; the print-hint floor prints the absolute-path command too. The unprivileged parent never depends on (pipes/captures) the privileged child's stdout. [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-TEMPLATE-ARGV-FILL required: [impl, unit, int] stages: -doc +impl +unit +int Command-template substitution fills argv ELEMENTS, not a re-tokenized string: spt-core currently `fill_template`s {key} values INTO the command STRING and THEN `tokenize`s the filled string (runtime.rs:94/122), so a multi-word {key} value whitespace-SPLITS into multiple argv tokens unless the adapter hand-quotes the placeholder, and a value containing a `"` (or `;`) injects/breaks tokenization (shell-injection-adjacent). A filled value MUST become exactly ONE argv element regardless of spaces/quotes in the value. Fix: tokenize the TEMPLATE into argv FIRST, then `fill_template` EACH token, so a `{key}` slot resolves to a single element and the value never participates in tokenization (no whitespace-split, no quote/semicolon injection); preserve the missing-key / empty-command errors and `{{`/`}}` non-interpretation. perri's F-009 (v0.8.1 dogfood, argv-capture-confirmed): a multi-word `{psyche_prompt}` = "PSYCHE REVIVAL time: epoch-ms:… incoming event: (none)" arrived as argv[6..12] (7 stray tokens), the harness runner strict-parsed `--prompt` against the 2nd word, exited 2 within ~1s → phantom hosted perch. Applies to EVERY [session.] template (psyche_init, extractor, notif, …); digest survives today only because its fills ({session_id}/{source}) are single-token. [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-UNHOST-PSYCHE-REAP required: [impl, unit, int] stages: -doc +impl +unit +int On un-host, the detached `{id}-psyche` HARNESS PROCESS is reaped — not just its in-brain pulse-driver thread. Today stop_host (livehost.rs:203) trips the HostedLife stop flag + JOINS the driver thread, but the Psyche is a detached harness process (spawn_psyche → ManifestRuntime detached spawn, runtime.rs:341-356; its pid is untracked in HostedLife though stamped on the `{id}-psyche` perch, where residency-confirm already reads it). So endpoint-stop / mid-life agent-death / a B2/B5 offline-then-unhost leaves the psyche process ORPHANED, alive until the next daemon-stop (where Breap's job/group reaps the whole brain subtree). The Psyche STAYS a harness process by design (CONTEXT.md 97/203/251 — headless harness session, its own perch) — the fix does NOT move it in-brain; it SCOPED-kills the `{id}-psyche` pid on un-host (never machine-wide — shared box). Track the pid in HostedLife at host_one (cleanest) or read the `{id}-psyche` perch pid at stop_host. Composes with H3 (endpoint stop → offline → reconcile un-host → reap) and B2/B5 (the offline arms that trigger un-host). (v0.12.0) [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-VIEWER-CLOSE-DETACH required: [doc, impl, unit, int] stages: +doc +impl +unit +int A VIEW is independent from the endpoint: closing the tab/window where `spt endpoint run` was invoked must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc `. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. FIX UPDATE (v0.12.1 L1.5, doyle re-scope operator-approved 2026-06-18): job-neutral daemon launch is now PRIMARY, breakaway DEMOTED to a fallback rung. ROOT reframed — the daemon INHERITS the terminal's Job because spawn_detached runs FROM the terminal-child CLI (DETACHED_PROCESS detaches the console, not the job); breakaway tried to claw back out but a job CAN deny it (the L1 finding). FIX: launch the cold-started daemon via a job-NEUTRAL creator so it is WmiPrvSE/Task-Scheduler-owned, OUTSIDE any terminal job from birth (why Task-Scheduler-autostarted daemons never had this bug). Launcher ladder (first-success-wins, daemon.rs spawn_detached → BOTH cold-start AND `spt daemon start`): (1) WMI Win32_Process.Create via ABSOLUTE powershell -EncodedCommand (KH 5.12 abs path; base64-UTF16LE dodges all quoting; success requires BOTH ReturnValue==0 AND a parsed ProcessId, else fall-through — never a silent launched), forwarding SPT_* env via a `cmd /c set … & start /b` wrapper because a WMI/scheduler child does NOT inherit transient shell env (verified — SPT_HOME would be lost, wrong universe); (2) schtasks one-shot (same env wrapper; best-effort fallback); (3) CREATE_BREAKAWAY_FROM_JOB (the L1 code, reordered below); (4) in-job last resort (logs DETACH_IN_JOB + tab-close caveat). detached_no_inherit (breakaway-then-in-job) is UNCHANGED for its other caller shellhost::launch_shell (a daemon-spawned shell is already job-neutral once the daemon is). The elevated deelevate path keeps its L1 breakaway for now (elevated-case WMI-reparent = FOLLOW-UP). (v0.12.1) [OK] REQ-HAZARD-VIEWER-ISOLATION required: [unit, int] stages: +doc +impl +unit +int A slow / dead / hostile VIEWER must NEVER stall the controller, the PTY child, or the session drain thread. The broker drain fans output to the controller on the authoritative blocking bounded path (advances delivered_through) but to each viewer via a bounded per-viewer channel with a dedicated writer thread; the drain `try_send`s under the log lock and a viewer whose bounded queue OVERFLOWS (can't keep up) is EVICTED (queue dropped, writer thread ends, removed from the viewers map) — the drain thread NEVER touches a viewer socket, so no viewer write can backpressure or block it. A soft viewer cap bounds the thread count. Viewer eviction never perturbs the controller stream, the delivered_through cursor, or the child. [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-WIN-PTY-PROGRAM-RESOLVE required: [doc, impl, unit, int] stages: +doc +impl +unit +int Native-PTY spawn must resolve a bare program name with PATHEXT precedence and run a non-PE target through its interpreter: portable-pty's own `which` takes the FIRST PATH match — an extensionless shebang shim (e.g. a node CLI `ccs` shipped beside `ccs.cmd`) — and CreateProcessW then rejects the non-PE file with os error 193 ('not a valid Win32 application'); spt-term resolves the program itself (PATHEXT order prefers .EXE over .CMD; .cmd/.bat → cmd.exe /d /c, .ps1 → powershell -NoProfile -File) so a bare harness/shell [session.self] command actually launches on Windows. Unix is a passthrough (execve honours the shebang). [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-HOST-RUN-1 required: [impl, unit, int] stages: -doc +impl +unit +int spt-hosted harness bringup: `spt endpoint run` spawns an adapter's `[session.self]` command template into a broker-held PTY (the spawn-session seam, brain.rs spawn_session_pid — same broker path shellhost.rs launch_shell_brokered_in uses for shells, now for kind="harness" self-role), registers the perch under the given endpoint id, returns the id. Reverses today's harness-hosted-only launch (external launcher → `api bind`). Non-interactive flag set (--adapter --id --create --resume --attach|--start|--view) covers every terminal action of the W2 interactive picker so shortcuts (cc-) bake fully non-interactive launches; composite adapter:profile resolves via registry::resolve_option leaf-replace overlay. [OK] REQ-HOST-RUN-2 required: [impl, unit, int] stages: -doc +impl +unit +int Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc ` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14). [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 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-10 required: [impl, unit] stages: -doc +impl +unit -int Windows at-logon autostart runs the daemon in the background with no persistent window: the scheduled task launches `spt daemon start` (which spawn_detaches a console-less DETACHED_PROCESS daemon and exits) rather than the foreground `spt daemon run` — Task Scheduler's interactive ONLOGON launch of a long-lived console process otherwise leaves a visible console window for the daemon's whole lifetime (v0.7.4) [OK] REQ-INSTALL-11 required: [doc, impl, unit] stages: +doc +impl +unit -int Adapter command templates resolve their program against the adapter's install dir BEFORE PATH: a `.spt`-shipped binary (dropped to adapters/_github// by --release/--github acquisition, or kept in the source_dir under copy-mode where only manifest+strings/ are copied to adapters/) runs without any PATH placement — a bare-name template token (e.g. `claude-spt-digest ...`) is rewritten to /(.exe on Windows) when that file exists, else left bare for the PATH fallback. Makes a `.spt` self-contained (closes the --release bundled-binary gap perri confirmed) (v0.7.4) [OK] REQ-INSTALL-12 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Durable active-profile pointer for bind-time profile selection (ADR-0021): adapters/active-profiles.toml at the registry ROOT (sibling to the per-adapter / dirs, so adapter add/update/remove — which only rewrite a / subdir — can never clobber it), a flat host_binary → "adapter[:profile]" map. Read at bind as the PRIMARY profile selector; unset → the registered_at_ms fallback (REQ-START-5). Written ONLY by `spt adapter use [:profile]` (resolves the adapter's host_binaries → sets each binary→adapter[:profile]); `spt adapter use --clear ` drops. NEVER auto-written by install/update/adapter add (that is precisely what would let an update silently flip the active profile). A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored, fall back, warn once. Pruned on adapter remove. Atomic write (spt_store atomic). (v0.9.0) [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: [impl, unit] 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: [impl] 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: [impl] 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-INSTALL-9 required: [doc, impl, unit] stages: +doc +impl +unit -int Adapter add from a GitHub release archive: `spt adapter add --release [--tag ] [--asset ]` fetches a `.spt` tar asset over HTTPS+GitHub trust, extracts it to the durable adapters/_github home, and registers the root — ships built binaries source-free and versioned (the distribution path for an adapter whose dev repo is a monorepo subdir, where --github root-only clone does not fit) [OK] REQ-KICK-1 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Explicit, loud controller displacement: `spt rc kick ` / `--take` (Take intent) kicks the incumbent controller and becomes controller; the displaced controller receives a LOUD `Displaced{by}` notice and is FULLY DETACHED (not demoted to a viewer). A default attach to a controlled endpoint is NEVER a silent displace (it is the Control busy-refusal). An old (N-1) rc omits intent → Control, so it can drive a free endpoint but CANNOT `--take` — it can never silently steal, and gets a clean busy-refusal instead. Taking control rides the same access_check(endpoint, origin, Unsolicited) as a normal control attach (if you may drive, you may take — no elevated kick policy). The picker surfaces 'Kick and attach' (Take) only on a controlled (blue ■) endpoint, via the existing attach dispatch (single-bringup-path: intent is a parameter). [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-MANIFEST-2 required: [doc, impl, unit] stages: +doc +impl +unit -int Adapter profiles — sparse leaf-replace overlays (shipped + local), composite : addressing, shadow-refusal, tighten-only consent floors [OK] REQ-MANIFEST-3 required: [doc, impl, unit] stages: +doc +impl +unit -int Adapter strings — [strings] KV tree, dot-path get-string resolving through the profile leaf-replace overlay, set-string editing a local profile's [strings] only; data-only (nothing executes a string) [OK] REQ-MANIFEST-4 required: [doc, impl, unit] stages: +doc +impl +unit -int Keyword hints — [[hints]] {keywords (literal/regex), text}; spt api hint --session emits at most one matched hint per message, once per session (seen-set), declaration-order first match; profiles overlay [[hints]] by leaf-replace [OK] REQ-MANIFEST-5 required: [doc, impl, unit] stages: +doc +impl +unit -int File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters//strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings]. [OK] REQ-MANIFEST-6 required: [doc, unit] stages: +doc -impl +unit -int Cross-adapter fallback target addressing (M12-W3-T3.2): a cross-adapter fallback target is addressed as `:` (not just a bare adapter_name), resolved through the one composite-addressing resolver (registry::resolve_option) at every adapter-option read site so a fallback may select a shipped/local profile (e.g. a `ccs` profile). CONTEXT.md §cross-adapter-fallback reconciled ("ccs is a profile; cross-adapter fallback may target :"). Contract-only this milestone: the node-wide fallback SETTING + its rate-limit invocation are deferred to the consuming milestone (the runtime path does not exist yet); this REQ guarantees the ADDRESSING resolves. [OK] REQ-MANIFEST-7 required: [doc, impl, unit] stages: +doc +impl +unit -int Adapter-declared shortcut basename (M12-W2 follow-on): an optional `[adapter] shortcut_basename` manifest field names the basename the `spt endpoint run` picker bakes into the generated `-` launcher shortcut (REQ-RUN-SHORTCUT). Absent ⇒ the harness-agnostic default `spt` (→ `spt-`); an adapter sets it to brand its shortcuts (claude-spt → `cc` → `cc-`), so the Claude-Code-ness lives in the PUBLISHED adapter manifest, never hardcoded in spt-core. The picker reads it from the RESOLVED manifest of the selected adapter (registry::resolve_option), falling back to `spt` when absent/empty/unresolvable. Additive + N-1-safe (serde-default Option, omitted from serialization when absent; old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). Documented in docs/MANIFEST.md `[adapter]` section + the claude-spt worked example — the adapter-author contract perri builds spt-claude-code against. [OK] REQ-MANIFEST-8 required: [doc, impl, unit, int] stages: +doc +impl +unit +int [adapter] host_binaries declares the harness executable basenames a kind="harness" adapter hosts agents inside (e.g. host_binaries = ["claude"]); bind-time pid→exe-basename match (case-insensitive, .exe-stripped) over the seed's parent_pid selects the candidate adapter set; zero matches → a friendly error naming the binary + the --adapter escape hatch. Additive + N-1-safe: optional Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] (omitted-serialized like shortcut_basename, old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). The match-key for ADR-0021 adapter-agnostic bind-time resolution. (v0.9.0) [OK] REQ-MESH-1 required: [impl, unit, int] stages: -doc +impl +unit +int Membership proof (seed-proof): symmetric current-epoch seed-knowledge replaces is_trusted at EVERY inbound gate (registry apply, WAN receive, sync, notif, connection accept). MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch); mutual channel-bound challenge-response at connect (transcript binds both handshake-proven node pubkeys, both nonces, subnet_id, seed_epoch, role); verified once per connection, cached on the broker ConnEntry, kept warm via QUIC keep-alive so re-proof is restart/partition/rotation-only. Exact-epoch match (re-seed is the sole N-1 exception). SECURITY INVARIANTS: channel-bound (no cross-connection replay), mutual, accepts a member it never paired (the mesh property). [OK] REQ-MESH-2 required: [impl, unit, int] stages: -doc +impl +unit +int Member roster: node-level union-merge grow-set (per member: pubkey, label, machine_id, last-known address, last-seen — NOT the seed), the discovery directory the mesh dials by. Seeded IN FULL at pairing (seed-holder hands joiner the whole current roster, incl. offline members — folds in deferred pairing-time hostname capture + post-join address seeding); each node authors its own entry stamped with its lease_epoch, merged strictly-greater-wins (the node_label lease); exchanged only over seed-proof'd member connections; forgery-inert (a fake entry names a pubkey that still can't seed-proof). Removal needs a TOMBSTONE — a per-pubkey revoked marker that propagates, dominates the entry, gates admission (seed-proof ∧ ¬tombstoned), and prevents reinsert; cleared by a completed re-pair of that pubkey. Persists through silence (offline member keeps its entry). [OK] REQ-MESH-3 required: [impl, unit, int] stages: -doc +impl +unit +int Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly. [OK] REQ-MESH-4 required: [impl, unit, int] stages: -doc +impl +unit +int Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke ...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair. [OK] REQ-MESH-5 required: [impl, unit] stages: -doc +impl +unit -int Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead. [OK] REQ-MESH-6 required: [impl, unit] stages: -doc +impl +unit -int Concurrent liveness probes: `spt subnet status --nodes` fans out its offline/serve-probes (REQ-SUBNET-5) CONCURRENTLY — total wall-time bounded by the single-probe ceiling (~3s), never k×ceiling. The mesh makes a node see ALL members (many possibly offline), so a serial probe loop would be offline_count×3s. (Planning verifies the current REQ-SUBNET-5 probe loop's behavior and fixes it if serial.) [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 otherwise, chunk oversized lines into EVENT-PART [OK] REQ-MSG-5 required: [doc, impl, unit] stages: +doc +impl +unit +int user-msg envelope kind + daemon identity gate: a Gateway endpoint / the local user's CLI author user-msg (the user's authority); agent-family senders re-stamped to plain msg; identity-gated never payload-trusted (KH 7.3/7.5); wire-additive (N-1 receivers tolerate the new type) [OK] REQ-MSG-6 required: [doc, impl, unit] stages: +doc +impl +unit -int cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace) [OK] REQ-MSG-ENVELOPE required: [doc, impl, unit, int] stages: +doc +impl +unit +int The body envelope (spt-proto::event, the ADR-0001 grammar) is the SOLE canonical arriving-message format at EVERY harness arriving-message surface on an AGENT perch — api listen AND api poll/worker-poll, byte-identical (reverses REQ-MSG-4's 'hook drains keep the raw frame by contract'). SCOPE CARVE-OUT: the shell-command relay (api poll --link, cmd_poll_shell) is a distinct internal transport carrying RAW MAC'd stamped frames the shell child consumes verbatim — NOT an arriving-message surface, deliberately EXEMPT from composition (notify_shell_e2e guards this boundary). __REPLY_TO__ — mis-elevated during the clean-room port to a fake ADR-0001 'stable wire format' (spt-msg/wire.rs, lib.rs) — is REMOVED entirely (spool format_row, the spt-msg TCP frame, emit parse_frame); (from, body) carried structurally, composed once at the delivery boundary. No legacy sister-interop (spt-core never required it). Reply-correlation rebinds onto the structural from / attribute (ADR-0009 access-gate + ADR-0012 Psyche/spt-live reply-target). Self-delimiting by construction → finding F-002 (non-self-delimiting multi-message poll) dissolves. ADR-0020. [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: [] 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: [impl, unit] 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-PICKER-1 required: [impl, unit] stages: -doc +impl +unit -int The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on EndpointRow from {offline | controllable | driven_by} with precedence offline→gray, else driven_by→blue, else !controllable→amber, else green (driven_by outranks harness-only; mutually exclusive in practice — a harness-only endpoint has no broker PTY to control). The controllable discriminator is a NEW InfoJson.controllable: Option (serde-default, N-1-safe), stamped at the establish seam — cmd_listen (harness-hosted relay, no broker PTY) → Some(false); cmd_bind live_agent (spt-hosted broker PTY) → Some(true); absent → not-controllable (amber) default (harness-hosted is the common mis-reported case; one bind self-corrects). Store-projection-only (no live daemon query — doyle ruling). (v0.10.0) [OK] REQ-PICKER-2 required: [impl, unit] stages: -doc +impl +unit -int The picker's project-history loader reads the git-backed context store, not the bare working tree: data.rs project_history_for enumerates an endpoint's projects via the BranchStore branch set (the context store keeps per-project context in git branches — contextstore::project_branch(project_id), checked out to projects/// only on-demand) instead of raw std::fs::read_dir over the empty working tree (which returned empty for ALL rows incl wall-a — the operator bug). Ordered newest→oldest by branch commit recency; degrades to empty (informational pane), never fails. (v0.10.0) [OK] REQ-PICKER-3 required: [impl, unit] stages: -doc +impl +unit -int A self-owned subnet row reconciles its status to the LIVE roster: a Subnet-category row whose endpoint_id overlaps a local (is_local) roster id is self-owned (this node hosts it), so its status square is OVERRIDDEN with the live roster status — the WAN registry snapshot (wansend::load_snapshots) is a periodically-advertised, independently-stale projection, while the local roster (p.alive) is ground truth for an endpoint this node hosts. One status square per endpoint (CONTEXT.md:348-350 — nothing licenses opposite squares for one endpoint across its Local vs Subnet listings). A reconcile pass in data.rs after the local_rows + subnet_rows gather; BOTH category listings are preserved (Local + Subnet are legitimately distinct views — you are in your own subnet), only the STATUS is unified. (v0.10.0) [OK] REQ-PICKER-4 required: [impl, unit] stages: -doc +impl +unit -int The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!("{l} ({}…)", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0) [OK] REQ-PICKER-5 required: [impl, unit] stages: -doc +impl +unit -int `spt endpoint list` (bare/subnet view) renders an ALIGNED table with canonical node labels: cmd_endpoint_list prints subnet rows with `\t` TAB separators (cli.rs:~1651-1662) so variable-width endpoint_ids snap fields to different tab-stops → a RAGGED status column (operator screenshot: X/help statuses misaligned vs rt-*/sptc-*/wall-a); and it calls the node renderer with no label → bare key-hex for every row (SAME ResourceRow-drops-node_label root as REQ-PICKER-4). FIX: max-width per-column padding (mirror render_node_rows' pad, pad by char count not byte len — '…' is multibyte) replacing the tabs, and render the node via the shared node_label_display now that ResourceRow carries node_label (REQ-PICKER-4). Extract a pure row-formatter seam so the alignment+label is unit-testable. ALSO: the bare list is the SUBNET view (a just-run LOCAL perch is invisible cross-subnet until the next advertise tick), so emit a `--local` hint line so a freshly-run endpoint isn't perceived as lost. (v0.10.0; operator-flagged + doyle dispatch 2026-06-17) [OK] REQ-PICKER-ADAPTER-DESCRIPTION required: [] stages: -doc -impl -unit -int The Create-new adapter-CHOICE screen of `spt endpoint run`'s picker shows a right-hand Description panel (like the Pick-existing endpoint picker's two-pane) surfacing per-adapter detail: install date, last-updated, adapter TYPE / the endpoint types it hosts, and the adapter description — so the user can see WHAT each adapter is before choosing it (today the selector lists bare names). DEFERRED fast-follow to v0.12.0 (operator 2026-06-18). (post-v0.12.0) [OK] REQ-PICKER-HISTORY-FRESH required: [impl, unit] stages: -doc +impl +unit -int The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1) [OK] REQ-PICKER-ONLINE-ACTION required: [impl, unit] stages: -doc +impl +unit -int The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1) [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-RC-1 required: [impl, unit, int] stages: -doc +impl +unit +int `spt rc ` — user CLI attaching a local terminal to a broker-held PTY, reusing the cross-node attach machinery (attach.rs request_attach → send_attach_input pump, spt-net AttachRecord codec); local attach is the degenerate single-node case of the cross-node path (rides REQ-TERM-3 byte-stream streaming). Read-only `--view` (watch, no stdin forwarded). Clean detach that does NOT terminate the broker-held session (KNOWN-HAZARDS: PTY ownership stays with the broker; no termination on detach). Explicit detach keybind that cannot collide with harness passthrough input (legacy capsule used a ctrl-b prefix); documented. ConPTY DSR auto-answer in the attach reader (hazard 5.5). [OK] REQ-RCVIEW-1 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Remote-attach controller/viewer model (CONTEXT.md:317): a session's broker OutputLog serves ONE interactive controller (input + EXCLUSIVE PTY resize; its viewport sets the size, sent on attach + every window change via crossterm Event::Resize) plus ANY NUMBER of read-only `--view` attachers (output-only, no input, no resize; client-side letterbox — center+pad when larger, clip+1-line indicator when smaller; only the local ctrl-b d detach chord). Attach intent is three-valued (`Viewer | Control | Take`, wire-default Control): Control to a FREE endpoint becomes controller, Control to a CONTROLLED endpoint is REFUSED with guidance (`--view`/`--take`) — never auto-viewer, never silent-displace. Wire adds (additive, N-1 skip-unknown): `Request.intent`, `Resize{rows,cols}` (controller-only), `Size{rows,cols}` (→viewer), `Displaced{by}` (→displaced controller). The brain-resume cursor (delivered_through, ADR-0018) tracks the CONTROLLER ONLY; viewers replay from their own from_seq and never move it. Dormancy keys on the controller ONLY: controller attach wakes / controller detach goes dormant (even with viewers present); viewer attach/detach is wake-neutral and may watch a dormant endpoint as-is. v1: viewing is gated identically to driving — a viewer runs the same access_check(Unsolicited) as a controller (watching reveals full session contents = a real disclosure); a lighter distinct watch-gate is deferred to cross-subnet/finer-consent (CONTEXT.md:317 'driving ≠ watching' = the future seam). [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-READY-AGENT-RESUME required: [doc, impl, unit, int] stages: +doc +impl +unit +int An offline ReadyAgent shows in `spt endpoint run`'s picker Resume-from-history and resumes correctly — closing the gap that today only LiveAgents do. ROOT: a harness-hosted ready bind (ReadyAgent::start_homed, ready.rs) writes info.json DIRECTLY and never appends the session ledger (unlike the shared establish_perch:250 live path), so a ready agent — though it has a session_id — produces ZERO ledger rows → the picker's offline+local Resume-from-history (which gates on ledger rows) never offers it. FIX (1): ledger the ready bind (ReadyAgent::start_homed → sessions::append Boot, mirroring establish_perch). FIX (2): `spt endpoint run --resume ` honors the adapter MANIFEST's endpoint TYPE — a ReadyAgent manifest (no [session.psyche_init]) resumes as a ready endpoint (poll listener, NO psyche-host); a LiveAgent (with psyche_init) as live. NO new bringup mode + NO picker changes (operator 2026-06-18): `spt endpoint run` is the spt-hosted ENDPOINT bringup for BOTH types, the type IS the adapter-manifest's concern (psyche-host already keys on psyche_init presence) — so (2) likely already holds; VERIFY at code, build only the residual. (v0.12.0) [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-RUN-PICKER required: [doc, impl, unit] stages: +doc +impl +unit -int Interactive `spt endpoint run` picker (ratatui TUI): bare `spt endpoint run` (no --adapter/--id) enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind="harness" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [ | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue "attached" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p- branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles ` @ (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path. [OK] REQ-RUN-SHORTCUT required: [doc, impl, unit] stages: +doc +impl +unit -int `-` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `-` launcher at the project root baking the current selection's non-interactive `spt endpoint run` flags (terminal actions only: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic spt-core defaults to `spt` (→ `spt-`); an adapter/flow OVERRIDES it (spt-claude-code → `cc`), so spt-core NEVER bakes `cc` (a harness name) into itself. The basename must be a DISTINCT token, never bare `spt` (a `spt.cmd` would shadow the real `spt.exe` only under cmd.exe cwd-first search, silently no-op in PowerShell/Unix, and self-recurse). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `` in the project dir / PowerShell `.\` / Unix `./`; a truly-bare basename on PATH = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely, but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe). [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-SEND-SPT-HOSTED required: [impl, unit, int] stages: -doc +impl +unit +int An inbound `spt send` is DELIVERED to an spt-hosted endpoint (brought up via `spt endpoint run` → `api bind`, broker holds its PTY, NO `api listen` relay). Today cmd_bind→establish_perch (api/startup.rs ~441) writes info.json + ready marker + controllable=Some(true) but registers NO message-listener / NO address, so deliver.rs resolve_address→None→spool (deliver.rs:132-140) and the message NEVER reaches the live PTY — the endpoint reads 'online' (ready marker) yet `spt send` silently SPOOLS ('online but not deliverable' lie). Per CONTEXT:187-188 the daemon owns the PTY and delivers, manifest-configurable per activity-state (direct PTY injection / relay / HTTP). FIX: route an inbound send for an spt-hosted target through the daemon → broker InputReq → session.write_input PTY-inject (broker.rs dispatch_input/write_input ~988-1022), the same path the brain uses; the live-delivery handshake must report Sent (not Queued) and stop the spool-only fallback for a broker-hosted, PTY-resident endpoint. Detection is local: controllable==Some(true) + spt-hosted state + resolve_address==None. = the spt-core HALF of the wall-b finding (perri owns the adapter half: bind-hook fired-zero-perch + the missing endpoint-run int test). (post-v0.10.0) [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 (CONTEXT Shell model) [OK] REQ-SHELL-2 required: [impl, unit, int] stages: -doc +impl +unit +int Shell sleep/wake: link-break always closes the binary (pre-close instruction + termination timeout), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake) [OK] REQ-SHELL-3 required: [impl, unit, int] stages: -doc +impl +unit +int Drive channel (owner->shell, REST-only, never-spooled, latest-wins): the owner->shell mirror of sensory for continuous real-time control (scroll/crank/stick/avatar) — a [shell.drive] manifest vocab + EVENT_TYPE_DRIVE frame, delivered to the ONLINE binary only via a single live slot (a new frame supersedes an undelivered one — no spool, no queue, no replay on relink), dropped-with-diagnostic if the shell is offline; cross-node rides the ephemeral link (REST class), never the durable shell spool. Commands = discrete+durable; drive = continuous+ephemeral (CONTEXT:260, minted 2026-06-11 Gateway grill). [OK] REQ-SHELL-4 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Shell tunnel (reliable-ordered opaque byte stream): an owner<->shell link may hold a long-lived, reliable-ordered, link-bound QUIC stream pair carrying opaque wire protocol traffic the channel taxonomy must NOT reinterpret (first consumer usbip URB) — manifest opt-in, not enveloped, not MAC-framed, not spooled; the link lifecycle governs it (a link-break closes the tunnel). Reliable-ordered ⇒ congestion surfaces as lag never loss ⇒ acceptable only on-LAN: the on-LAN posture is documented and the tunnel is NOT proven cross-WAN (CONTEXT:262, minted 2026-06-11 Gateway grill; doyle gate C2). [OK] REQ-SHELL-5 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Shell ownership is owner-type-agnostic: any non-Shell endpoint type may own/spawn/drive/command/link a shell (Gateway the named first) — control-exclusivity keys on the owner endpoint_id, NEVER on the owner's endpoint type. No ownership path (mint, launch, owner-from-link, cmd, drive, tunnel, sleep/wake, owner-shutdown) inspects the owner's type (CONTEXT:264, ratified 2026-06-11 Gateway grill). [OK] REQ-START-1 required: [impl, unit] stages: -doc +impl +unit -int Adapters never resolve SPT_HOME; binary on PATH; api bridging only [OK] REQ-START-2 required: [impl, unit, int] stages: -doc +impl +unit +int Harness-hosted startup: api seed then listen [OK] REQ-START-3 required: [impl, unit, int] stages: -doc +impl +unit +int spt-hosted startup: spawn-session then api bind (no file) [OK] REQ-START-4 required: [impl, unit] stages: -doc +impl +unit -int Adapter-injected env aliases (SPT/OWL/LIVE) [OK] REQ-START-5 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Adapter-agnostic harness-hosted seed + bind-time adapter/profile resolution (ADR-0021): `api seed` carries only parent_pid + session_id (+ optional cwd), no --adapter — a pure "a harness session exists at this pid" record; --adapter becomes an OPTIONAL override across the whole api group (an explicit name[:profile] for adapter dev, never required). Omitted, listen/poll resolve the owning adapter/profile AT BIND as a pure read against the live registry — never a seed-time snapshot that can drift: seed parent_pid → exe basename → host_binaries candidate set (REQ-MANIFEST-8) → active-profile pointer (REQ-INSTALL-12) primary, else greatest-registered_at_ms candidate base profile (name-asc tie) → friendly zero-match error. Covers BOTH LiveAgent (listen) and ReadyAgent (poll) bringup. Restores legacy parity: `$LIVE start ` → `$SPT listen ` with no mandatory --adapter, one generic SessionStart hook per harness binary. (v0.9.0) [OK] REQ-STORE-1 required: [impl, unit] stages: -doc +impl +unit -int spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011) [OK] REQ-SUBNET-1 required: [impl, unit] stages: -doc +impl +unit -int spt subnet noun namespace: status view (bare + status [NAME] [--nodes]), create (QR/otpauth), show-code; spt pair deleted [OK] REQ-SUBNET-2 required: [impl, unit, int] stages: +doc +impl +unit +int Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder [OK] REQ-SUBNET-3 required: [impl, unit] stages: -doc +impl +unit -int Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity) [OK] REQ-SUBNET-4 required: [impl, unit] stages: +doc +impl +unit -int Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment) [OK] REQ-SUBNET-5 required: [impl, unit, int] stages: -doc +impl +unit +int Per-subnet serve-state: spt subnet detach [--save] / attach [--save] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --save persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6, --save renamed from --auto per decision 25 session) [OK] REQ-SUBNET-6 required: [impl, unit] stages: -doc +impl +unit -int Trust lifecycle verbs, elevation-gated: spt subnet leave (membership exit) and spt subnet prune (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7) [OK] REQ-SUBNET-7 required: [impl, unit] stages: -doc +impl +unit -int Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22) [OK] REQ-SUBNET-8 required: [impl, unit] stages: -doc +impl +unit -int Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23) [OK] REQ-TERM-1 required: [impl, unit] stages: -doc +impl +unit -int Process-supervisor terminal wrapper hosting broker PTYs [OK] REQ-TERM-2 required: [impl, unit] stages: -doc +impl +unit -int session-surface abstraction; send-keys + send-line injection [OK] REQ-TERM-3 required: [impl, unit] stages: -doc +impl +unit -int Byte-stream remote terminal streaming for v1 [OK] REQ-TERM-4 required: [impl, unit, int] stages: -doc +impl +unit +int Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push [OK] REQ-TERM-5 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL. [OK] REQ-TERM-6 required: [impl, unit, int] stages: -doc +impl +unit +int Thread-spanning digest across session boundaries: a per-endpoint session ledger (`/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session. [OK] REQ-TERM-7 required: [impl, unit, int] stages: -doc +impl +unit +int Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them. [OK] REQ-UPD-1 required: [impl, unit, int] stages: -doc +impl +unit +int Peer-propagated update over P2P [OK] REQ-UPD-2 required: [impl, unit] stages: -doc +impl +unit -int All binaries signature-verified before handoff [OK] REQ-UPD-3 required: [impl, unit, int] stages: -doc +impl +unit +int No endpoint process terminates/suspends during self-update [OK] REQ-UPD-4 required: [impl, unit] stages: -doc +impl +unit -int Update gated on user confirmation by default; opt-in full-auto [OK] REQ-UPD-5 required: [impl, unit] stages: -doc +impl +unit -int spt-core ripple-updates registered adapters [OK] REQ-UPD-6 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016) [OK] REQ-UPD-7 required: [impl, unit] stages: -doc +impl +unit -int Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified. [OK] REQ-UPD-8 required: [impl, unit] stages: -doc +impl +unit -int Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent. [OK] REQ-UPD-9 required: [doc, impl, unit] stages: +doc +impl +unit -int `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8). [OK] REQ-WHOAMI-1 required: [doc, impl, unit] stages: +doc +impl +unit -int `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).