PAIR 1: REQ-ACL-FAIL-CLOSED <-> REQ-ACL-VIEW-DRILLDOWN ================================================================================ A) REQ-ACL-FAIL-CLOSED Title: A corrupt or unreadable access store degrades CLOSED, loudly (ADR-0053 — an ADR-level flip of ADR-0009's deliberate fail-open ruling). ADR-0009 justified fail-open explicitly: the whitelist was 'a same-subnet convenience boundary layered inside subnet membership, not the system's outer wall', because every member node was the one user's own machine. The SHARED SUBNET ruling breaks that premise — member nodes now belong to different human operators and the access layer gates agents — and a boundary that evaporates when its store file corrupts is not a boundary an operator can reason about. So a store that CANNOT BE READ refuses unlisted-subject traffic on every surface rather than admitting it, and says so on the refusing node naming the store path (silence plus refusal reads as network failure and burns diagnosis time). TWO THINGS SURVIVE THE DEGRADE: the stateful-firewall reply exemption (correlation state is a separate file keyed on the endpoint's own recent outbound — a degraded node keeps its own conversations alive) and the same-node loopback Allow. THE SPLIT THAT MAKES THIS SHIPPABLE: an ABSENT store file is NOT a degrade. It is the unconfigured baseline — every fleet node today has no access.json, and reading absence as closed would refuse all unsolicited cross-node traffic fleet-wide on upgrade. The two cases split on std::io::ErrorKind::NotFound and nothing else: never-created reads as an empty store whose chain bottoms out open; any other io error or a parse failure is a degrade. Absent-is-open keeps an unconfigured fleet reachable, unreadable-is-closed makes the boundary real. Consequence accepted per ADR-0053: a personal fleet with zero configured rules now blocks unsolicited cross-node traffic if its store file CORRUPTS — the failure is loud, local, and repairable, and the alternative (silent admission on a shared subnet) is the worse lie. Existing fail-open tests are repinned to the new contract as part of this diff (a product contract change, not a test fixup). Gate: doc — ADR-0053 and the CONTEXT.md control-surface-modes entry carrying the degrade rule; impl — AccessStore::load_checked/load_checked_from returning AccessDegraded (path + cause) with the NotFound split, and the daemon gate's refuse-plus-diagnostic arm ordered AFTER the reply exemption; unit — the degrade split (absent is baseline, corrupt and unreadable both degrade, diagnostic names the path) and the gate-level proof that the same call is allowed before corruption, refused on every surface during it, still allowed as a reply throughout, and allowed again once the store is repaired without a restart. Doc: control-surface modes (`open` / `closed`)** (ratified 2026-07-29, access-control grill): Per-surface default posture for unlisted subjects — `open` = allowed (no forced whitelisting), `closed` = blocked. Defined at three levels: **subnet** (a universal all-surfaces mode chosen at `subnet create` — prompted with **no preselection**, flags `--open`/`--closed`; per-surface customization later only via an *empower*ed engine-room), **node** (set via the node's engine-room, member-or-admin TOTP), and optionally **per-endpoint** (exists only if deliberately set). **Resolution — first match wins, mode B) REQ-ACL-VIEW-DRILLDOWN Title: The granular rule list is viewable only per named ruled entity (CONTEXT.md 'access entity'): `spt endpoint access [--endpoint-rules | --node-rules | --subnet-rules ]` — mutually exclusive flags — renders exactly the rules whose subject names that entity, through the SAME ruleset-table renderer the engine-room briefing carries (REQ-ER-RULESET-TABLE), filtered on the TYPED rule subject rather than by re-parsing the rendered subject word (a rendered-string match is a second literal waiting to drift). A target positional scopes the slice to that endpoint's rules plus the node-scope rows that also decide for it; the drill's `--json` emits the rows verbatim. The one-flat-dump-of-everything view is deliberately gone — reading a posture is a comparison per entity, not a scroll. Gate: doc — the CONTEXT.md access-entity entry; impl — the typed filter seam and the flag surface; unit — the filter matches only its own entity kind (a subnet name equal to an endpoint id must not cross-match) and the flags parse mutually exclusive. Doc: access entity** (ratified 2026-07-30, fast-follow grill): Anything that can be granted (or denied) control via access rules — a **subnet**, **node**, or **endpoint**. A **ruled access entity**, relative to a given target, is an access entity that at least one of the target's rules names. Access views are **roster-first**: a target lists its ruled access entities grouped by type (subnets, then nodes, then endpoints), each with its rule count (and, for a subnet or the home node, its mode); the **granular rule list is viewable only per named ruled entity**, and external entities with no explicit PAIR 2: REQ-ACL-RC-VIEW-SPLIT <-> REQ-SEC-1 ================================================================================ A) REQ-ACL-RC-VIEW-SPLIT Title: Watching an endpoint's terminal and DRIVING it are separately grantable: attach gates on the request's AttachIntent — Viewer -> RC_VIEW, Control/Take -> RC_ATTACH. Before this, one access_check(endpoint, origin, Unsolicited) covered every attach intent, so admitting a node to view an endpoint necessarily admitted it to take the keyboard (and, with Take, to displace an incumbent controller). On a shared subnet that is the difference between showing a colleague's agent what happened and letting their agent drive yours. The split is keyed on the intent the REQUEST carries, evaluated at the serve side under the handshake-proven origin — never on anything the attaching side can restate after the gate. Kin: ADR-0042 (rc-attach truth) and REQ-ACL-SURFACE-VOCAB, which mints the two ids. Gate: doc — the CONTEXT.md control-surface entry naming RC_VIEW and RC_ATTACH as distinct v1 surfaces; impl — the attach_surface mapping and attach.rs gating through it; unit — the mapping over all three intents plus a decision table proving an RC_VIEW grant does not admit RC_ATTACH. Doc: control surface** (ratified 2026-07-28, access-control grill): The unit of access-control granularity: a named remote-reachable operation class on an endpoint. **Open string vocabulary, CONSTANT_CASE ids** (like capability ids — new surfaces mint ids without schema change). v1 set = the existing gate families: `MSG`, `RC_VIEW`, `RC_ATTACH`, `DIGEST`, `WAKE`, `SUSPEND`, `XFER`, `SHELL_LINK`, `DISCOVER`. Later waves (remote endpoint-info, adapter package serving, webservice facets) mint their ids when the capability itself is built. An access rule is (target endpoint × surface × subject-chain) → B) REQ-SEC-1 Title: Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants Doc: PAIR 3: REQ-CONSENT-2 <-> REQ-HAZARD-WAN-ORIGIN-AUTH ================================================================================ A) REQ-CONSENT-2 Title: 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) Doc: B) REQ-HAZARD-WAN-ORIGIN-AUTH Title: 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) Doc: 7.5 WAN-inbound origin is transport truth, never payload `[REQ-HAZARD-WAN-ORIGIN-AUTH]` Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface. Invariant:** the origin the gate (and detection/UX — "node X is driving") consumes is the **QUIC handshake-proven remote node id** (iroh `EndpointId` == Ed25519 node pubkey PAIR 4: REQ-ADAPTER-ADD-SURFACE-ERRORS <-> REQ-HAZARD-DAEMON-SCHED-NONBLOCKING ================================================================================ A) REQ-ADAPTER-ADD-SURFACE-ERRORS Title: Bug #1: adapter add runs the install-as-first-update via conduct (cli.rs:6963) which on a non-zero exit prints only the exit code and DISCARDS the subprocess stdout/stderr, so the real error is invisible (the failure itself does propagate). Fix: include out.stderr/stdout in the ADAPTER_INSTALL_FAIL message (mirror run_update_post_step). Operator ruling: ALSO run the [update.post] composite step at install-time (today it runs only on explicit adapter update), so an install both surfaces detail and completes the delegated post-step. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #1. Doc: B) REQ-HAZARD-DAEMON-SCHED-NONBLOCKING Title: 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) Doc: PAIR 5: REQ-ADAPTER-GH-TRANSPORT <-> REQ-HAZARD-ECHO-BEFORE-SIGNOFF ================================================================================ A) REQ-ADAPTER-GH-TRANSPORT Title: The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2) Doc: adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025): A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `/` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. B) REQ-HAZARD-ECHO-BEFORE-SIGNOFF Title: Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3) Doc: PAIR 6: REQ-ADAPTER-TEMPLATE-KEY-VALIDATION <-> REQ-MANIFEST-6 ================================================================================ A) REQ-ADAPTER-TEMPLATE-KEY-VALIDATION Title: P-1 core half (WORKER-TRUTH triage): `adapter add`/`adapter update` validate every declared role template — command, cwd, and [env] inject values — against the substitution-key catalog (spt-runtime BASE_KEYS + role-specific overrides) and REFUSE registration naming the offending key + role (fail-fast family of the [strings] pointer validation). Field driver: flynn's psyche died on the RETIRED {psyche_dir} key (adapter psyche_resume carried the old psyche_init cwd shape) — a permanent template config fault must die loudly at registration, not at the Nth per-event psyche turn via the 3-strike budget. The catalog stays the single source (runtime.rs FILL_KEYS — 'a catalog key must have a real fill'); validation reads it, never a second list. Doc: B) REQ-MANIFEST-6 Title: 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. Doc: Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it. A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github//` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binari PAIR 7: REQ-ADAPTER-VERSION-CMD <-> REQ-RESUME-CONTEXT-PULL ================================================================================ A) REQ-ADAPTER-VERSION-CMD Title: `spt adapter version ` prints a registered adapter's declared version — the EXISTING mandatory `[adapter].version` manifest field (manifest.rs already requires it; NOT a `[strings].version`, NOT `get-string`, no second version source). A new `AdapterCmd::Version{option}` resolves the option's merged view via `registry::resolve_option` like the sibling adapter subcommands and prints `manifest.adapter.version`; an unresolvable option errors (exit 1) the same way. (v0.13.2 W6) Doc: `version` is **mandatory**. It is the single declared adapter version — read before any update (the `gh_release` compare point), surfaced by `spt adapter version ` (which prints this `[adapter].version` of a registered adapter exit 1 if unregistered), and the value the post-update `message` gate keys on. There is no second version source — no `[strings].version`, no `get-string` convention. B) REQ-RESUME-CONTEXT-PULL Title: Adapter-callable resume-context pull verb + not-yet-synthesized commune/signoff drop append (legacy-SPT parity, operator-directed 2026-06-24). GAP: spt-core exposes NO verb for a harness adapter's SessionStart hook to pull an agent's resume context — `resume::download_psyche_context` (spt-live/src/resume.rs:88, composes ++ from the durable two-tier store) is INTERNAL with ZERO spt callers and no ApiCmd verb (api/mod.rs ApiCmd enum has none); resume.rs:9 documents the intended 'adapter pulls it in its SessionStart hook' path but it was NEVER wired. Result: a harness adapter cannot inject the agent's durable mind on resume at all (claude-spt today runs only `api boundary` session-rotation + an identity brief — the agent resumes WITHOUT its mind). TIER-1 SCOPE (operator-approved; Tier-2 = drift-stamp//drift-directive + + Pulse-Log DEFERRED to a separate parity item, NOT v0.15.0 — the legacy download_payload [claude_skill_owl context.rs:344] is richer but memformat is roadmap-deferred + drift-stamp is an orthogonal cross-machine-drift feature). TWO PARTS: (1) EXPOSE `spt api psyche-download [--session-id ]` -> stdout = the composed brief, project_id resolved from the endpoint's bound cwd (info::read_info -> cwd -> project derive; NO --project arg), auth-gated like sibling id-scoped verbs (the `gated(&id,&auth,…)` pattern); empty store -> NO-CONTEXT on stderr (mirror legacy). The adapter SessionStart hook runs it + injects stdout as additionalContext. (2) APPEND any commune/signoff drop NOT YET SYNTHESIZED into the durable tiers as a distinct / slice AFTER the durable slices. GATING (operator ruling): append while NOT-YET-SYNTHESIZED, NOT merely 'while the raw file is on disk' — in today's synchronous ingest (ingest_drops route_two_slice writes durable THEN deletes the file, lifecycle.rs:466 @ DEFAULT_PULSE_PERIOD 5s) the two coincide (a watched-dir drop IS pre-synthesis), so the v1 realization reads the manifest-declared session.commune_dir/signoff_dir (manifest.rs:208/210) for a present -commune.md/-signoff.md (COMMUNE_SUFFIX/SIGNOFF_SUFFIX, ingest.rs); the CONTRACT keys on synthesis-state so it stays correct when async Psyche synthesis lands (a consumed-but-not-yet-committed drop stays appended via a pending-synthesis staging set — forward hook). The agent-checkpoint trigger sentinel CHECKPOINT_SENTINEL=`!!checkpoint!!` (a FIXED spt-core constant — operator-specified, CONTEXT.md §fixed-constants, NOT adapter-configurable) is stripped at BOTH drop-body points via one shared `strip_checkpoint_markers` (remove every token, keep inter-marker text, collapse trivial whitespace): the PRE-synthesis pending-append (resume::append_pending) AND the POST-synthesis durable ingest (ingest::route_slices — the single choke covering route_two_slice + signoff.write_resume_commune; strip-then-empty-filter so a marker-only slice routes nowhere) — else the marker would persist PERMANENTLY in live-context.md once a checkpoint drop synthesizes + re-trigger once the adapter's checkpoint detection is live. PRESENTATION-ONLY: the append NEVER writes the durable store (spt-core remains sole store-writer, REQ-HAZARD-DROP-FILE-SINGLE-WRITER; mirror legacy's read-only/process_file_drop-sole-deleter discipline). SELF-CLEARING: once synthesis commits the slice vanishes — no duplication. CORE-OWNED (not adapter): an adapter-side raw-file read RACES spt-core's ingest-delete (TOCTOU, ingest.rs:161 removes the drop on pulse-consume); the fold MUST live in the single composer all resume pulls flow through. New public CLI verb -> docs-drift gate (xtask gen + reference.md no-internal-codes, cli-command-docs-drift). (v0.15.0 parity wave W5) Doc: resume-session seam** — two distinct forms: fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. That command is **`spt api psyche-download [--session-id ]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPE PAIR 8: REQ-API-3 <-> REQ-WHOAMI-IDENTITY-ONLY ================================================================================ A) REQ-API-3 Title: commune/signoff are file-drops, not commands Doc: B) REQ-WHOAMI-IDENTITY-ONLY Title: PROJECT-INDEX W1 (F-040, perri filing claude-spt docs/SPT-CORE-FINDINGS.md @d775b38; correctness-critical opener — the 2026-07-15 message-bodies incident root): a core IDENTITY-ONLY resolution — session -> endpoint|null — that touches NO list/registry/project/git/network path, and `spt whoami` DE-ALIASED from cmd_endpoint_list (cli.rs ~6609 aliases the full list = 100+ git children under hook deadlines). endpoint-info is DISQUALIFIED as the carrier (runs latest_project_ref). Adapters/hooks get a bounded-time identity verb; the harness-hosted adapter fallback stays deadline-vulnerable until this ships. Gate: impl — the resolver + whoami de-alias; unit — resolver returns endpoint|null with zero project derivation (assert no git spawn seam); int — whoami on a multi-perch home answers fast-path without touching context branches; doc — harness-contract api.md names the identity verb + its no-derivation bound. Kin REQ-PROJECT-INDEX-READER-CUTOVER (list-shaped verbs), REQ-WHOAMI-1, docs/PROJECT-INDEX-TRIAGE.md. Doc: The bounded-time "which endpoint am I?" answer for hooks and adapter glue: resolves the calling session to its endpoint (`$OWL_SESSION_ID` `$SPT_AGENT_ID` / process ancestry) and prints that ONE endpoint's SELF line — id, liveness, description. **The no-derivation bound is the contract**: whoami never enumerates the roster, never derives projects, never runs git, never touches the network — safe to call from deadline-bounded hook paths (the class that previously timed out and black-holed message delivery). Unresolved is a clean answer, not an error stall: `NO_PERCH` on stderr (`--json`: `{"id" PAIR 9: REQ-DIGEST-SEAL-ON-IDLE <-> REQ-MANIFEST-7 ================================================================================ A) REQ-DIGEST-SEAL-ON-IDLE Title: A digest turn's records gain their stable `seq` at the endpoint's IDLE TRANSITION — the adapter-reported, already-authoritative turn-finished signal — not at next-user-input arrival (which is demoted to a harmless no-op fallback seal). Kills the mint-then-idle deadlock (liam/spt-progress-tool W16: a finished-but-idle turn stayed partial/seq-less, so a seq-keyed scanner could never see the owner's latest turn until the owner was prompted again; interim drive-one-more-command workaround retires at ship). Sealing is IDEMPOTENT and SEQ-STABLE: it assigns seq to what the log holds at idle; a late-flushing straggler record folds into the sealed turn WITHOUT changing the assigned seq. (ADR-0048 decision 4; rejected: extractor-observed completion = banned heuristic inference class; provisional seqs = scanner bug factory.) Gate: int — the liam field shape RED-first: finish a turn, report idle, do NOT prompt again — the turn is sealed with a stable seq and a scanner keyed on seq sees it; fallback leg: next-input on an already-sealed turn re-seals nothing; straggler leg: a post-idle log record folds in, seq unchanged. Doc: A turn closes when the endpoint goes idle — not when the next input arrives.** The moment the endpoint reports it stopped working, the trailing turn is finished: `partial` drops away and its entries take their seqs, with no further prompting needed. This matters if you scan for something in the latest** turn: you no longer have to wait for (or manufacture) another user input before that turn has a stable cursor. A later input still closes the previous turn as it always did — that path simply is not the only one any more. / 4. The idle transition seals the digest turn B) REQ-MANIFEST-7 Title: 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. Doc: `shortcut_basename` *(optional, default `spt`)* — the basename the `spt endpoint run` picker's `s` keybind bakes into the generated `-` launcher shortcut at the project root (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic `spt` (→ `spt-`); an adapter sets it to brand its shortcuts — `claude-spt` uses `cc`, giving `cc-doyle`. spt-core never hardcodes a harness name; the picker reads this from the **resolved** manifest of the selected adapter. The launcher is the current OS's native form (`.cmd` on Windows — `.ps1` is excluded by the default `PATHEXT`; a POSIX `sh` `+chmod +x` PAIR 10: REQ-ECHO-DROP-DIR-RESOLVE <-> REQ-EP-7 ================================================================================ A) REQ-ECHO-DROP-DIR-RESOLVE Title: W1 (LIFECYCLE-TRUTH): fire_echo resolves the manifest commune_dir through the SAME resolver its siblings use before any write. ROOT (pinned): fire_echo (spt-daemon lifecycle.rs:790) passes the RAW manifest commune_dir into run_echo_commune -> echo.rs:115-117 create_dir_all+join; a relative `.claude` under the WMI-launched daemon's System32 cwd = os error 5 deterministic (two live psyches stamped FAILED on it). Siblings already resolve correctly (ingest ~:583, psyche_drop_file :1072 via resolve_endpoint_drop_dir(raw, cwd)). FIX: fire_echo routes through resolve_endpoint_drop_dir; relative-with-no-cwd = SKIP LOUD (stderr), never a raw relative write — kills the latent-worse variant where a writable daemon cwd writes the drop to a WRONG dir silently (echo communes lost, no error). Hardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write; echo claude spawn gets explicit cwd = endpoint cwd (perri ask). Doc: B) REQ-EP-7 Title: 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) Doc: live role** (`live-role.md`, ratified 2026-06-12 — core milestone A): A durable statement of an agent's **broad purpose** — rarely modified, and only at deliberate user instruction. Lives in `tracked/` (the mind) beside `live-context.md`, so it replicates with the mind and follows the agent across nodes. At start-transition context injection it renders **first** (role, then live context, then project context). The guarantee is **mechanical**: no automated writer exists — Psyche reconcile, echo-communes, and signoff structurally never touch it; the sole writer is `spt endpoint role [--overwrite PAIR 11: REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP <-> REQ-UPDATE-ADAPTERS-VERB ================================================================================ A) REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP Title: A DELEGATED live adapter apply MUST NEVER report success without performing the swap, and the live-update seam MUST use ONE parent-aware adapter matcher across all its comparators. TWO defects made the field repro (BUILD-F015B-APPLYMATCH: `--adapter cc:ccs` live update silently no-ops): (D1, matcher skew) the broker's dispatch_adapter_apply filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter :` endpoint stores the COMPOSITE `cc:ccs` while the apply carries the PARENT record name `cc` — so every :profile endpoint fell out to affected=[]; select_endpoints_running_adapter had the same `adp == adapter` skew, while the CLI live-gate (adapter_has_live_endpoint) already parent-matched — divergent rules on ONE seam. (D2, silent success) the affected.is_empty() branch replied KIND_APPLIED and RETURNED WITHOUT SWAPPING; once the CLI delegates the apply there is no CLI-side fallback swap, so success-without-swap = the update never lands (re-register re-reads the OLD manifest, version-of-truth honestly says old). FIX: (1) ONE shared spt_runtime::profile::adapter_parent_matches(session_adapter, parent) used by the live-gate + broker apply-filter + select_endpoints_running_adapter (no exact `==` against a record name at any live-update seam); (2) the daemon owns the whole apply once delegated — the CRC swap runs UNCONDITIONALLY (terminate/restart loops no-op when nothing is resident), KIND_APPLIED reported ONLY after a real swap. (F015B, ADR-0025 amendment) Doc: 7.24 A delegated live adapter apply must NEVER report success without swapping, and the live-update seam must use ONE parent-aware adapter matcher `[REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP]` Failure (F015B / BUILD-F015B-APPLYMATCH):** a live adapter update to a PROFILE-COMPOSITE endpoint (`--adapter cc:ccs`) silently no-oped — `LIVE` then `DONE` printed, but the version-of-truth stayed OLD. TWO defects on one seam. **(D1, matcher skew):** the broker's `dispatch_adapter_apply` filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter :` endpoint stores the COMPOSITE B) REQ-UPDATE-ADAPTERS-VERB Title: THE-FORKENING W4 (operator-grilled 2026-07-14): `spt update adapters [[,...]]` = thin ALIAS over the existing `spt adapter update` engine (cli.rs:748 gh_release avenue; the old verb STAYS — published surface) + comma-list accepted on BOTH forms. Semantics: no names -> all gh_release-avenue registrations; names validated FAIL-FAST against the registry BEFORE any update starts (a typo must not leave a half-updated set); per-adapter failure ISOLATION (one failure doesn't stop the rest) with a per-adapter summary line; nonzero exit if any failed; local-path/dev registrations SKIP loud (not error). Gate: unit — name validation, list parsing, isolation + exit-code aggregation, local-path skip; doc — reference regen (drift-gated). Kin REQ-UPDATE-DEFAULT-COMPOSITE (the caller), REQ-ADAPTER-UPDATE-MESSAGE (per-adapter apply notices ride the summary). Doc: update composite (`spt update`)** — the plain verb is the primary form: `update fetch --apply` then `update adapters` (core-first order); with core already current, only adapters update. `--core-only`/`-c` skips adapters; `spt update adapters [[,…]]` is the adapters leg alone (alias over `spt adapter update`). The composite's invoker always survives, because a routine apply cycles only the **brain** — the *restart-required* message on broker-side releases is a notice, not a restart. `spt update --restart` is the one-step **full cycle**: fetch → adapters → `apply --finish` last (the finis PAIR 12: REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE <-> REQ-WHOAMI-EXPLICIT-SID-REFUSAL ================================================================================ A) REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE Title: F-032 (perri field finding 2026-07-08, LEGACY-SPT-PARITY-GAP, data-loss): commune/signoff ingest MUST NOT delete a drop until its content is DURABLY COMMITTED to every APPLICABLE tier — a slice that cannot be committed this ingest must leave the drop in place for a later ingest (retry when the precondition resolves) OR durably preserve the un-committed slice, NEVER delete-then-lose. ROOT (doyle triage, code-grounded): ingest_drops (spt-live/src/ingest.rs:200) unconditionally `remove_file(&drop_path)?` AFTER route_slices (ingest.rs:121), but route_slices GATES the project tier on `!project_id.is_empty()` (ingest.rs:156) — when the endpoint's cwd is unresolved/owlery-internal at ingest time the project_id is empty, so the `` slice is PARSED but never write_context'd/commit_project'd, yet the source drop is still deleted → the project-context content is permanently lost (black-hole). perri's repro: a two-sliced echo-commune ( role+release recipe + v0.17.4 status + Items 3-5 map) INGESTED (file deleted) yet never surfaced at her next SessionStart resume-pull; adapter exonerated (file-write + slicing tags correct); fixture at (system temp)/F-032-commune-2026-07-08T222721Z.md (5595B, sha256 9bc27e18cf385958; perri wrote it verbatim from session log 0841835d.jsonl). Legacy spt held commit-first-then-delete parity; the modern two-slice ingest broke it. FIX distinguishes: a write SUPPRESSED-by-precedence (incoming older than durable → already-superseded → safe to delete) from a slice NOT-committed-because-un-committable-now (empty project_id / write error → must NOT delete; retry or preserve). Gate: an ingest with a non-empty slice but an EMPTY project_id must NOT delete the drop (or must durably preserve the project slice) — the content survives to the next resolvable ingest / SessionStart; the live-tier commit path stays unchanged; a genuinely superseded (precedence-suppressed) drop still deletes. KNOWN-HAZARDS entry on landing (REQ-HAZARD-* = conformance-checklist, needs a test). Doc: 7.40 A commune/signoff drop is deleted ONLY after every applicable tier is durably committed — an un-committable slice preserves the drop, never delete-then-lose `[REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]` Failure (paid-for, perri field finding 2026-07-08 — F-032, data-loss):** `ingest_drops` unconditionally deleted the drop after `route_slices`, but the project tier is GATED on a non-empty `project_id` — when the endpoint's anchor cwd was unresolved/owlery-internal at ingest time, the `` slice was parsed but never committed, yet the source drop was still deleted → the project-con B) REQ-WHOAMI-EXPLICIT-SID-REFUSAL Title: RULED DESIGN, delivery unowned (doyle 2026-07-26): when a caller hands identity resolution an EXPLICIT non-empty $OWL_SESSION_ID that resolves to NO perch, core must REFUSE identity (unresolved, exit 1, loud distinct diagnostic) rather than fall through to an ambient/inherited one — today `detect_self_id` (roster.rs, legs a→b→b2→c) treats sid-UNMATCHED identically to sid-ABSENT, so the fallback chain re-adopts precisely the identity a sharper claim just failed to prove. MEASURED (perri, this node, 2026-07-26, three read-only whoami calls from a genuine descendant of the perri host process): (1) all SPT_*/OWL_* scrubbed → id null, exit 1 — ancestry resolved nothing (caveat honored from the probe: the perch's recorded pid was not in the caller's chain, so this run refutes lineage-as-the-mechanism for probe v1 without disproving a lineage path in general); (2) inherited SPT_ENDPOINT_ID=perri + explicit OWL_SESSION_ID matching no perch → perri, exit 0 — the mismatch datum was IN HAND (core had already scanned and failed to match the explicit sid) and the ambient id won anyway; (3) real OWL_SESSION_ID with endpoint id scrubbed → correct self — the healthy path any fix must leave untouched. SCOPE OF THE REFUSAL, ruled: only sid-PRESENT-AND-UNMATCHED poisons the fallback, and it poisons ALL weaker legs (b SPT_AGENT_ID, b2 SPT_ENDPOINT_ID, c pid-ancestry) — an explicit failed claim outranks every ambient claim below it; sid-ABSENT/empty keeps today's full chain unchanged, because the leg-b2 field root (live-repro'd 2026-07-10: the adapter surfaces OWL_SESSION_ID to the session shell as an UNEXPORTED var, so the child process legitimately carries endpoint id without sid) is exactly the flow the guard must not break — that flow is sid-absent, never sid-mismatched. WHY CORE AND NOT ONLY THE ADAPTER: the measured entry path is closed adapter-side (perri's REQ-HAZARD-INHERITED-IDENTITY-ADOPTION @ their a7558aa + claude-spt KNOWN-HAZARDS 7.4, shipped: whoami child calls scrub SPT_ENDPOINT_ID/SPT_AGENT_ID; rig discipline now detached AND env-scrubbed — the scrub is the operative half), but the inconsistency being fixed is CORE'S: the adapter fastpath's verified_env_id REFUSES an inherited SPT_ENDPOINT_ID on carrier-proof mismatch (carrier sid != payload sid) and core's fallback then RE-GRANTS what that layer just refused — one layer's refusal must not be another layer's grant, and every other harness/adapter gets the defense only if core holds it. SEVERITY UPGRADE recorded at mint: unlike the KH 7.1–7.3 shapes (lost reads), this adoption was WRITE-CAPABLE in the field — the adopting descendant re-pointed the ANCESTOR's session pin, so the ancestor went dark while the descendant looked healthy (perri's own pin, probe v1). KIN, same review same milestone: leg (b) SPT_AGENT_ID returns UNCONDITIONALLY today — not even perch-checked, weaker than leg b2's bound-perch gate; align it when this lands. MEASURED on the live node, not just code-read (perri, same probe run, reported 2026-07-26): SPT_AGENT_ID=nobody-xyz with a bogus OWL_SESSION_ID returned {'id':'nobody-xyz','ready':false,'alive':true,'unbound':false}, exit 0 — a phantom identity for an endpoint that does not exist, beating both the sid leg and ancestry; the returned shape has no state key and ready:false but a populated id, and the adapter parser takes .id first, so a whoami-trusting adapter writes state under the phantom — the same write-capable class as the inherited-adoption case, sourced from a made-up name instead of a real ancestor. perri's adapter scrub covers SPT_AGENT_ID as well as SPT_ENDPOINT_ID for exactly this reason. POSTURE UNCHANGED: whoami legs remain from-label/routing only, never authentication (KH 7.3/7.5, F-024 stays parked) — refusal tightens label discipline, it promotes nothing to auth. Gate at activation: unit — sid-unmatched + ambient endpoint id present → refusal with the distinct diagnostic (probe shape 2 goes loud); sid-absent + ambient endpoint id on a bound perch → still resolves (leg-b2 field root preserved); sid-matched → unchanged (probe shape 3). Doc: PAIR 13: REQ-HAZARD-SUBPROCESS-TIMEOUT <-> REQ-READY-AGENT-RESUME ================================================================================ A) REQ-HAZARD-SUBPROCESS-TIMEOUT Title: Every harness/git subprocess has a timeout (5.3) Doc: B) REQ-READY-AGENT-RESUME Title: 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) Doc: `spt endpoint run` is the spt-hosted bringup for BOTH endpoint types** (v0.12.0): The bringup core is **type-agnostic** — the endpoint TYPE is the adapter manifest's concern, not a separate bringup mode. A manifest declaring `[session.psyche_init]` brings up a **LiveAgent** (the daemon reconcile hosts its Psyche); a manifest *without it brings up a **ReadyAgent** (a poll listener, no Psyche — see *ReadyAgent* and the harness-hosted ready bind at the *seed + bind-time resolution* note above). No `--adapter`/picker branch distinguishes them: the daemon live-host reconcile hosts only a perch whos PAIR 14: REQ-MANIFEST-7 <-> REQ-RESUME-CONTEXT-PULL ================================================================================ A) REQ-MANIFEST-7 Title: 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. Doc: `shortcut_basename` *(optional, default `spt`)* — the basename the `spt endpoint run` picker's `s` keybind bakes into the generated `-` launcher shortcut at the project root (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic `spt` (→ `spt-`); an adapter sets it to brand its shortcuts — `claude-spt` uses `cc`, giving `cc-doyle`. spt-core never hardcodes a harness name; the picker reads this from the **resolved** manifest of the selected adapter. The launcher is the current OS's native form (`.cmd` on Windows — `.ps1` is excluded by the default `PATHEXT`; a POSIX `sh` `+chmod +x` B) REQ-RESUME-CONTEXT-PULL Title: Adapter-callable resume-context pull verb + not-yet-synthesized commune/signoff drop append (legacy-SPT parity, operator-directed 2026-06-24). GAP: spt-core exposes NO verb for a harness adapter's SessionStart hook to pull an agent's resume context — `resume::download_psyche_context` (spt-live/src/resume.rs:88, composes ++ from the durable two-tier store) is INTERNAL with ZERO spt callers and no ApiCmd verb (api/mod.rs ApiCmd enum has none); resume.rs:9 documents the intended 'adapter pulls it in its SessionStart hook' path but it was NEVER wired. Result: a harness adapter cannot inject the agent's durable mind on resume at all (claude-spt today runs only `api boundary` session-rotation + an identity brief — the agent resumes WITHOUT its mind). TIER-1 SCOPE (operator-approved; Tier-2 = drift-stamp//drift-directive + + Pulse-Log DEFERRED to a separate parity item, NOT v0.15.0 — the legacy download_payload [claude_skill_owl context.rs:344] is richer but memformat is roadmap-deferred + drift-stamp is an orthogonal cross-machine-drift feature). TWO PARTS: (1) EXPOSE `spt api psyche-download [--session-id ]` -> stdout = the composed brief, project_id resolved from the endpoint's bound cwd (info::read_info -> cwd -> project derive; NO --project arg), auth-gated like sibling id-scoped verbs (the `gated(&id,&auth,…)` pattern); empty store -> NO-CONTEXT on stderr (mirror legacy). The adapter SessionStart hook runs it + injects stdout as additionalContext. (2) APPEND any commune/signoff drop NOT YET SYNTHESIZED into the durable tiers as a distinct / slice AFTER the durable slices. GATING (operator ruling): append while NOT-YET-SYNTHESIZED, NOT merely 'while the raw file is on disk' — in today's synchronous ingest (ingest_drops route_two_slice writes durable THEN deletes the file, lifecycle.rs:466 @ DEFAULT_PULSE_PERIOD 5s) the two coincide (a watched-dir drop IS pre-synthesis), so the v1 realization reads the manifest-declared session.commune_dir/signoff_dir (manifest.rs:208/210) for a present -commune.md/-signoff.md (COMMUNE_SUFFIX/SIGNOFF_SUFFIX, ingest.rs); the CONTRACT keys on synthesis-state so it stays correct when async Psyche synthesis lands (a consumed-but-not-yet-committed drop stays appended via a pending-synthesis staging set — forward hook). The agent-checkpoint trigger sentinel CHECKPOINT_SENTINEL=`!!checkpoint!!` (a FIXED spt-core constant — operator-specified, CONTEXT.md §fixed-constants, NOT adapter-configurable) is stripped at BOTH drop-body points via one shared `strip_checkpoint_markers` (remove every token, keep inter-marker text, collapse trivial whitespace): the PRE-synthesis pending-append (resume::append_pending) AND the POST-synthesis durable ingest (ingest::route_slices — the single choke covering route_two_slice + signoff.write_resume_commune; strip-then-empty-filter so a marker-only slice routes nowhere) — else the marker would persist PERMANENTLY in live-context.md once a checkpoint drop synthesizes + re-trigger once the adapter's checkpoint detection is live. PRESENTATION-ONLY: the append NEVER writes the durable store (spt-core remains sole store-writer, REQ-HAZARD-DROP-FILE-SINGLE-WRITER; mirror legacy's read-only/process_file_drop-sole-deleter discipline). SELF-CLEARING: once synthesis commits the slice vanishes — no duplication. CORE-OWNED (not adapter): an adapter-side raw-file read RACES spt-core's ingest-delete (TOCTOU, ingest.rs:161 removes the drop on pulse-consume); the fold MUST live in the single composer all resume pulls flow through. New public CLI verb -> docs-drift gate (xtask gen + reference.md no-internal-codes, cli-command-docs-drift). (v0.15.0 parity wave W5) Doc: resume-session seam** — two distinct forms: fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. That command is **`spt api psyche-download [--session-id ]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPE PAIR 15: REQ-PSYCHE-ROLE-OPTIONAL-SKIP <-> REQ-RESUME-HARNESS-SESSION-ID ================================================================================ A) REQ-PSYCHE-ROLE-OPTIONAL-SKIP Title: W1 (LIFECYCLE-TRUTH): a manifest with NO [session.echo_commune] role SKIPS commune-sync (debug-level note, no strike) instead of hard-failing the turn. ROOT (perri filing, recovered): missing role -> commune-sync hard-fails -> 3-strike stamps the host ('manifest declares no [session.echo_commune] role') while the published contract presents the role as an optional template. FIX: missing OPTIONAL role = skip, not a turn failure. Doc: B) REQ-RESUME-HARNESS-SESSION-ID Title: B2 (F028, hall-b diagnosis, verified 0.22.0): respawn/`--resume` feeds the SPT session id to `claude -r`. After the 02:05 daemon bounce respawn built `claude.exe -r 70b5bfa40901b7d4` — an spt session id in claude's OWN session-id namespace -> claude hangs forever at a 'No sessions match' resume-picker while the endpoint reads online. Hits after EVERY daemon bounce + every picker Resume. The HARNESS session id (claude UUID, stamped in sessions.log/info.json by the hooks) is what {session_id} must mean in the adapter's [session.resume] command; the spt sid must not leak. FIX: substitute the HARNESS session id in the resume template (spt-core substitution-key semantics + LIKELY claude-spt manifest coordination — FLAG perri BEFORE touching the manifest, adapter-boundary rule). Int: resume template receives the ledger UUID, not the spt sid. See triage B2. Doc: PAIR 16: REQ-SEAM-UPDATE <-> REQ-START-4 ================================================================================ A) REQ-SEAM-UPDATE Title: Adapter-update avenue (file-pull / delegated command) Doc: B) REQ-START-4 Title: Adapter-injected env aliases (SPT/OWL/LIVE) Doc: PAIR 17: REQ-BROKER-ATTACH-JOURNAL-RESILIENT <-> REQ-DAEMON-REFRESH ================================================================================ A) REQ-BROKER-ATTACH-JOURNAL-RESILIENT Title: A poisoned EffectJournal mutex or a sick NetHost runtime must NOT permanently brick all future attaches. Bug #16 (URGENT): a live spt-hosted endpoint (eel-a) attach fails with 'brain IPC read deadline elapsed' after a self-update brain-respawn — the broker survives the respawn and one journaled op (dispatch_net_stream_open journal.apply_once + loopback open_stream runtime.block_on nethost.rs:1060) enters a bad state, so every journaled attach silently kills its per-conn reply thread while non-journaled ops keep working. Fix: recover PoisonError via into_inner (effect.rs apply_once, replace the .expect panics) so one panic cannot brick all attaches; bound the loopback open_stream block_on (nethost.rs:1060) like the QUIC bounded_block_on so a sick runtime fails fast with an error frame not an opaque 10s deadline. Reinforces REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #16. Doc: B) REQ-DAEMON-REFRESH Title: THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah down — today's only remedy is a full daemon bounce that kills every PTY). Broker + PTYs survive by construction (handoff invariant). Failure = the existing trial rollback semantics (old brain resumes; refresh reports loud). Gate: unit — verb routes the brain-cycle without staging/swap preconditions; int — refresh on a live daemon with a hosted PTY: brain generation changes, PTY survives, endpoint stays attached; doc — daemon docs name refresh next to stop/start. Kin apply_staged (the path it reuses), REQ-UPDATE-TRIAL-DRAIN-DRIVE, REQ-BRAIN-RESUME-NO-CONTROL-STEAL. Doc: daemon refresh (`spt daemon refresh`)** — restart the **brain** in place, no binary swap, broker and every held PTY untouched: the routine-update handoff path minus the swap. The recovery verb for wedged brain-held state (broken endpoint bringup, a downed hosted agent) that previously required a full daemon bounce. PAIR 18: REQ-BIND-HONEST-SELF-STAMP <-> REQ-HAZARD-ROSTER-GHOST ================================================================================ A) REQ-BIND-HONEST-SELF-STAMP Title: C2-ROOT (F028, doyle RCA 2026-07-03): the identity-attribution ROOT behind REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION — three composing defects let a psyche-hosted SessionStart hook stamp a FOREIGN perch: (1) roster::detect_self_id leg (a) scans the owlery ONE level (roster.rs:107 read_dir(owlery)) so a NESTED psyche perch owlery//nested/-psyche can never self-resolve; (2) [session.psyche_init] (claude-spt manifest ~L347) spawns the psyche with NO env_remove + no pinned child identity, so whatever OWL_SESSION_ID/SPT_AGENT_ID reaches the child resolves to a foreign top-level perch; (3) the hook then writes info.json (session_id/pid rebind) + sessions.log on the mis-resolved victim. FIX (spt-core half): (i) identity PINNED at spawn — ManifestRuntime role spawns inject the child's OWN SPT_AGENT_ID= + OWL_SESSION_ID=, AND detect_self_id enumerates NESTED perches (fix the one-level owlery scan); (ii) BIND HONESTY — a session-start stamp may only write a perch whose resolved id AFFIRMATIVELY matches; never a fallback pick; refuse + loud-skip when unresolved (kin REQ-MSG-CLI-ORIGIN honest-default + #9 ancestry). ADAPTER half = PERRI touchpoint (psyche_init env scrubbing / relies on runtime pinning; hook loses silent fallback-perch behavior) — FLAG doyle BEFORE any manifest move (adapter glue-model rule). See triage C2 fix (i)+(ii). Doc: B) REQ-HAZARD-ROSTER-GHOST Title: 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) Doc: PAIR 19: REQ-CLI-2 <-> REQ-WHOAMI-EXPLICIT-SID-REFUSAL ================================================================================ A) REQ-CLI-2 Title: 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) Doc: B) REQ-WHOAMI-EXPLICIT-SID-REFUSAL Title: RULED DESIGN, delivery unowned (doyle 2026-07-26): when a caller hands identity resolution an EXPLICIT non-empty $OWL_SESSION_ID that resolves to NO perch, core must REFUSE identity (unresolved, exit 1, loud distinct diagnostic) rather than fall through to an ambient/inherited one — today `detect_self_id` (roster.rs, legs a→b→b2→c) treats sid-UNMATCHED identically to sid-ABSENT, so the fallback chain re-adopts precisely the identity a sharper claim just failed to prove. MEASURED (perri, this node, 2026-07-26, three read-only whoami calls from a genuine descendant of the perri host process): (1) all SPT_*/OWL_* scrubbed → id null, exit 1 — ancestry resolved nothing (caveat honored from the probe: the perch's recorded pid was not in the caller's chain, so this run refutes lineage-as-the-mechanism for probe v1 without disproving a lineage path in general); (2) inherited SPT_ENDPOINT_ID=perri + explicit OWL_SESSION_ID matching no perch → perri, exit 0 — the mismatch datum was IN HAND (core had already scanned and failed to match the explicit sid) and the ambient id won anyway; (3) real OWL_SESSION_ID with endpoint id scrubbed → correct self — the healthy path any fix must leave untouched. SCOPE OF THE REFUSAL, ruled: only sid-PRESENT-AND-UNMATCHED poisons the fallback, and it poisons ALL weaker legs (b SPT_AGENT_ID, b2 SPT_ENDPOINT_ID, c pid-ancestry) — an explicit failed claim outranks every ambient claim below it; sid-ABSENT/empty keeps today's full chain unchanged, because the leg-b2 field root (live-repro'd 2026-07-10: the adapter surfaces OWL_SESSION_ID to the session shell as an UNEXPORTED var, so the child process legitimately carries endpoint id without sid) is exactly the flow the guard must not break — that flow is sid-absent, never sid-mismatched. WHY CORE AND NOT ONLY THE ADAPTER: the measured entry path is closed adapter-side (perri's REQ-HAZARD-INHERITED-IDENTITY-ADOPTION @ their a7558aa + claude-spt KNOWN-HAZARDS 7.4, shipped: whoami child calls scrub SPT_ENDPOINT_ID/SPT_AGENT_ID; rig discipline now detached AND env-scrubbed — the scrub is the operative half), but the inconsistency being fixed is CORE'S: the adapter fastpath's verified_env_id REFUSES an inherited SPT_ENDPOINT_ID on carrier-proof mismatch (carrier sid != payload sid) and core's fallback then RE-GRANTS what that layer just refused — one layer's refusal must not be another layer's grant, and every other harness/adapter gets the defense only if core holds it. SEVERITY UPGRADE recorded at mint: unlike the KH 7.1–7.3 shapes (lost reads), this adoption was WRITE-CAPABLE in the field — the adopting descendant re-pointed the ANCESTOR's session pin, so the ancestor went dark while the descendant looked healthy (perri's own pin, probe v1). KIN, same review same milestone: leg (b) SPT_AGENT_ID returns UNCONDITIONALLY today — not even perch-checked, weaker than leg b2's bound-perch gate; align it when this lands. MEASURED on the live node, not just code-read (perri, same probe run, reported 2026-07-26): SPT_AGENT_ID=nobody-xyz with a bogus OWL_SESSION_ID returned {'id':'nobody-xyz','ready':false,'alive':true,'unbound':false}, exit 0 — a phantom identity for an endpoint that does not exist, beating both the sid leg and ancestry; the returned shape has no state key and ready:false but a populated id, and the adapter parser takes .id first, so a whoami-trusting adapter writes state under the phantom — the same write-capable class as the inherited-adoption case, sourced from a made-up name instead of a real ancestor. perri's adapter scrub covers SPT_AGENT_ID as well as SPT_ENDPOINT_ID for exactly this reason. POSTURE UNCHANGED: whoami legs remain from-label/routing only, never authentication (KH 7.3/7.5, F-024 stays parked) — refusal tightens label discipline, it promotes nothing to auth. Gate at activation: unit — sid-unmatched + ambient endpoint id present → refusal with the distinct diagnostic (probe shape 2 goes loud); sid-absent + ambient endpoint id on a bound perch → still resolves (leg-b2 field root preserved); sid-matched → unchanged (probe shape 3). Doc: PAIR 20: REQ-CLI-BROKEN-PIPE-TOLERANT <-> REQ-MSG-2 ================================================================================ A) REQ-CLI-BROKEN-PIPE-TOLERANT Title: #10 (F026 micro): `spt | ` must not PANIC when stdout closes early. A closed downstream reader mid-print panics with 'failed printing to stdout: The pipe is being closed. (os error 232)' (live repro: `spt daemon status | Select -First N`). Fix: tolerate BrokenPipe process-wide — a write to a closed stdout exits 0 (SIGPIPE-equivalent: a consumer that stops reading is a normal end, not a crash), without leaking a Rust panic + backtrace to the user. Doc: B) REQ-MSG-2 Title: spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes Doc: PAIR 21: REQ-CLI-OUTPUT-MARKDOWN <-> REQ-ER-RULESET-TABLE ================================================================================ A) REQ-CLI-OUTPUT-MARKDOWN Title: Human-prose COMMAND OUTPUT (not just `--help`) renders the inline Markdown authored in its source strings as terminal styling, never literal markers: `` `code` `` → ANSI cyan, `**bold**` → ANSI bold, `[text](url)` → `text`, markers STRIPPED either way. REQ-CLI-HELP-MARKDOWN only hooked the clap `--help` chokepoint, so command output still printed raw Markdown (audit: `spt how-to` topic text showed `# headers`/backticks, `spt subnet`/`subnet status` hint footers showed stray backticks, the daemon-status `not running` line, the `ENDPOINT_RUN_STARTED` attach hint, and the daemon's `SUBNET_DETACHED` startup line — 13 prose surfaces). The same line-bounded pure `helpfmt::render` is applied at each emit site, color-gated by the OUTPUT STREAM's own tty (`stdout_color` for print/println, the new `stderr_color` for eprintln). HARNESS-SAFETY (binding): color is tty-gated, so an adapter (piped / non-tty / NO_COLOR) gets STRIP mode = zero ANSI + markers removed; every dual-contract MACHINE token on a rendered line (`ENDPOINT_RUN_STARTED:`, `NO_SUCH_TOPIC:`, `SUBNET_DETACHED:`) carries NO Markdown markers, so it survives strip byte-intact — the adapter parse is never perturbed. Pure-machine output (the `` envelope, bringup parse-tokens SEEDED/BOUND/READY/NO_SEED, `--json`, QR) is NEVER routed through the renderer. The one spt-daemon source string (`SUBNET_DETACHED`, the bin-local renderer is unreachable from the daemon crate) is authored marker-free instead. (v0.12.2) Doc: B) REQ-ER-RULESET-TABLE Title: The engine room presents access rulesets as TABLES (ADR-0052 decision 3). The rendering is a requirement rather than a nicety because the operator decision this surface exists to support — is this node's posture what I think it is — is a comparison across subjects, surfaces and tiers, and prose forces a human to hold that grid in their head while an agent narrates it to them. A table also makes an omission visible: a row that should be there and is not is legible in a grid and invisible in a paragraph. It rides the same briefing message the session opens with (REQ-ER-SESSION-BRIEFING) and the same renderer serves an on-demand ruleset request. Gate: doc — ADR-0052 decision 3's table clause; impl — the ruleset table renderer used by the briefing and by an on-demand request; unit — the renderer emits one row per rule with subject, surface, tier and decision, renders an empty ruleset as an explicit empty table rather than silence, and is stable enough to diff across two postures. Doc: Refuses all inbound except replies to its own outbound (knocks and knock-codes ARE accepted); online **only while a controller is attached** — detach drops it offline and every empowerment dies with it; `rc --view` denied even locally; remote attach denied; **local `rc --take` allowed** precisely because it forces a harness restart and revokes all empowerments; not registry-advertised by default (only to endpoints it has whitelisted); every session start delivers a briefing message stating its capabilities and responsibilities; it presents access rulesets as tables. PAIR 22: REQ-DIGEST-CURSOR <-> REQ-HAZARD-DETACHED-PIPE-INHERIT ================================================================================ A) REQ-DIGEST-CURSOR Title: `spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last ` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after ` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0) Doc: Turn boundaries — classify delivered messages as `input` (binding).** The projection treats a `role: "input"` record as the **turn boundary** (the unit `--last`/`seq` count). An adapter's `[digest]` extractor / `api digest-entry` therefore **MUST classify a delivered user-facing message as a turn-opening `input`** record (equivalent to a direct PTY user-input) — not as `agent`/`tool` output. If messaging-delivered turns are not opened as `input`, a messaging-driven session collapses into a few giant turns and `--last ` / `seq` lose their granularity. *What* becomes an `input` is the adapter B) REQ-HAZARD-DETACHED-PIPE-INHERIT Title: 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) Doc: 5.6 Windows detached children inherit a captured caller's pipe `[REQ-HAZARD-DETACHED-PIPE-INHERIT]` Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::nul PAIR 23: REQ-ENDPOINT-LIST-MERGE-LOCAL <-> REQ-SHELL-3 ================================================================================ A) REQ-ENDPOINT-LIST-MERGE-LOCAL Title: `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) Doc: endpoint list always merges local perches**: `spt endpoint list` (and therefore `whoami`) **always** appends this node's **LOCAL perch roster** as a trailing section, in addition to the SELF pin and the subnet groups. The subnet groups are the WAN registry snapshot, which lags a just-bound perch by a pump cadence — so without the merge a freshly-online endpoint (or the caller's own, under `whoami`) could be **absent** from its own listing, which reads as lost. The earlier `--local` flag (a separate this-node-only view) is **removed**: the local view is no longer a mode, it is unconditionally p B) REQ-SHELL-3 Title: 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). Doc: PAIR 24: REQ-ENDPOINT-LIST-REST-FILTER <-> REQ-REST-VERB-ROUTING ================================================================================ A) REQ-ENDPOINT-LIST-REST-FILTER Title: spt endpoint list hides SUSPENDED instances by default; a new --show-all flag reveals them. Status-first row ordering with fixed precedence ONLINE > CONTROLLED > UNBOUND > SUSPENDED (when shown) > corrupt last, alphabetical by id within each band. Two invariants: (1) CORRUPT rows ALWAYS render regardless of filters — corrupt is a record condition demanding operator action (purge/re-mint), not resting clutter; hiding it would re-create counter-39 bug #3 (cross-ref REQ-HAZARD-CORRUPT-PERCH-COHERENCE, CONTEXT.md instance-state _Also avoid_); (2) the per-node Total line DISCLOSES the filter — 'Total: N (+M suspended hidden)' — so nothing silently vanishes. Registry-Offline rows stay excluded by projection law (resource_projection skips unroutable; unchanged). Grill-with-docs ruling 2026-07-02 (operator + doyle). Doc: B) REQ-REST-VERB-ROUTING Title: A-3 (REMOTE-TRUTH triage §A + Q3 operator-law): a BARE-id rest verb (spt wake/suspend ) routes across the subnet like send's fallback instead of failing local-only. ROOT (certain): cmd_rest (cli.rs:3296) gates the remote arm on id.contains('@'|':'); a bare id falls to the local-only arm (cli.rs:3340) → daemon_rest_event → info::read_info miss (resting.rs:248) → 'WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch'. cmd_send (cli.rs:5142) DOES fall back on a local miss; cmd_rest's remote arm (cli.rs:3307, wan_rest) already handles every WanRestOutcome — it is simply never reached on a bare-id local miss. Contradicts CONTEXT:286 'a wake must route'. Q3 SUBSTRATE GAP: resolve_across_visible (registry.rs:971) filters only by Status::routable() and its Ambiguity payload is node-hexes-only — it CANNOT express the Q3 status rule; per-candidate (node,status) comes from SubnetRegistry::instances(id). FIX: a NEW pure select_rest_target helper (status-aware, isolated from resolve_across_visible which cmd_send keeps) applying GOAL-SATISFACTION semantics (ADR/triage addendum @188d269, NOT naive verb symmetry — the mixed case breaks symmetry): wake is an ∃-goal (satisfied when ANY instance Active), suspend is a ∀-goal (satisfied when ALL instances Suspended); one helper parameterized by the verb's satisfaction predicate — 0 candidates→NotFound; goal already satisfied→NoOp naming the satisfying node(s); exactly 1 ACTIONABLE (not-at-target) instance→Act(node); >1 actionable→Ambiguous(copy-paste id@node list). Edge rulings: wake with >1 Active = NoOp naming ALL active nodes (NOT Ambiguous — nothing actionable); suspend mixed (X suspended + Y active, NOT ∀-satisfied) = Act(Y) if exactly one active / Ambiguous if several active. Candidate status is ADVERTISED/gossiped (post-A-1 shared-derivation, may be STALE) so a NoOp verdict is ADVISORY and the qualified id@node path is the operator override (noted in the helper doc-comment). cmd_rest's bare-id local miss loads snapshots → instances(id) → select_rest_target → dispatches (Act→wan_rest to the node / NoOp naming node(s) / Ambiguous render_refusal copy-paste id@node list / NotFound NO_ENDPOINT), all F-1 public language from day one. Qualified id@node path unchanged; shutdown leg-2 stays LOCAL_ONLY. Red-first: a bare id present ONLY in a remote registry snapshot routes to that node instead of WOKE_FAIL. Doc: PAIR 25: REQ-HAZARD-DETACHED-PIPE-INHERIT <-> REQ-RC-MOUSE-FORWARD ================================================================================ A) REQ-HAZARD-DETACHED-PIPE-INHERIT Title: 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) Doc: 5.6 Windows detached children inherit a captured caller's pipe `[REQ-HAZARD-DETACHED-PIPE-INHERIT]` Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::nul B) REQ-RC-MOUSE-FORWARD Title: On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0) Doc: 7.20 `spt rc` must forward the scroll wheel to the harness (our mouse capture steals WT's native scroll) `[REQ-RC-MOUSE-FORWARD]` Failure (operator HITL):** scroll stopped working in `spt rc`. P1's `EnableMouseCapture` (for right-click paste, 7.18) makes Windows Terminal forward ALL mouse — including the wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen under the capture). Invariant:** on Windows, TRACK the harness's mouse-reporting mode from its OUTPUT — scan for DECSET `E PAIR 26: REQ-MSG-SELF-DETECT-ANCESTRY <-> REQ-SHELL-LIST-DERIVED-PROVENANCE ================================================================================ A) REQ-MSG-SELF-DETECT-ANCESTRY Title: #9 (F026, operator field bug): a perch-owned `spt send` from an endpoint's OWN session must self-identify, not mis-stamp `cli@` (whose replies bounce NO_PERCH). ROOT: roster::detect_self_id (roster.rs) was ENV-ONLY — OWL_SESSION_ID matched to info.session_id, else SPT_AGENT_ID — but an agent-session Bash child often carries NEITHER (the env export is spawn-path-dependent), so a perch-owned sender was classified bare-CLI and REQ-MSG-CLI-ORIGIN stamped it cli@ (the stamp works as designed on a wrong premise; that REQ's evidence stays intact). FIX: detect_self_id gains leg (c) PID-ANCESTRY fallback AFTER the env legs — walk THIS process's ancestry, match a live non-corrupt roster perch's recorded harness pid (info.json.pid Numeric, alive-gated via is_process_alive, corrupt/BUSY skipped), first match = self. from-LABEL / routing default ONLY, NOT authentication — authenticate() is untouched (pid-ancestry-for-AUTH stays parked per F-024 with its Windows pid-spoof caveats; a display/routing stamp has no such bar). Best-effort: a broken ancestry walk degrades to None → cli-stamp, never errors the send. Doc: B) REQ-SHELL-LIST-DERIVED-PROVENANCE Title: SEED (inactive — observability, perri-backed 2026-07-26): a status a reader can act on should be distinguishable from a status a record actually holds. Since REQ-HAZARD-SHELL-STALE-ONLINE, `shell list` renders DERIVED online-ness (recorded status AND pid-liveness), deliberately healing poisoned records at the read gate — but nothing marks WHICH rows were healed, so an agent doing forensics reads the healed view and infers a clean record (field, twice on 2026-07-26: liam inferred close_shell had run when the raw info.json still said online over a corpse pid and proposed spending flynn's live instance to manufacture a specimen they already owned; perri independently named the same misread class from their own work). Shape at activation: an additive marker on the --json row when derived != recorded (e.g. recorded_status alongside status, or derived=true) — additive-evolution posture, text view unchanged or minimally annotated; NEVER a behavior change to the derivation itself. Kin: REQ-DAEMON-BITS-AMBIGUITY (silent wrong-state a human catches only by staring at the right field) and the NEVER-SEALING-OBSERVABILITY candidate — same class, view-vs-truth. Doc: PAIR 27: REQ-HAZARD-CONTROL-STAMP-CONVERGENCE <-> REQ-PICKER-CONTROLLED-LOCAL ================================================================================ A) REQ-HAZARD-CONTROL-STAMP-CONVERGENCE Title: Control/viewer stamps must CONVERGE to broker session-table truth for every session-backed endpoint, not merely edge-trigger — the UPWARD companion to the DOWNWARD edge-clear REQ-HAZARD-CONTROL-STAMP-LIFETIME (7.27); same 'stamps == broker truth' family. ROOT (F-026 stamp-gap, hall-b/ball-b): a picker-created endpoint's broker spawn become_controller->stamp_driven_by->set_controlled(true) fires BEFORE the adapter binds its perch (a fresh endpoint has no perch until claude boots + binds), so mutate_info returns NotFound and the edge stamp is SWALLOWED (let _); the adapter's bind then writes InfoJson::new with controlled:false DEFAULT and no later edge re-stamps -> the endpoint reads uncontrolled FOREVER while driven (the #3 display fix is correct but datum-starved on this creation path). FIX: the broker (SINGLE WRITER) re-asserts each live session's control/viewer stamps to session-table truth, DIVERGENCE-GATED (read info; compare driven_by/controlled/viewer_count; write ONLY on diff — no per-poll fsync storm), on the KIND_SESSIONS handler (piggyback: the daemon reconcile + picker poll it, so a fresh perch converges within one reconcile-poll window after bind = the BOUNDED window, no new timer). Event-on-input rejected (an idle controlled session like hall-b never converges). Writes run OFF the log lock (snapshot truth under the lock, converge off it) per the lock-across-effect discipline (KH 7.12/5.16). Doc: 7.29 Control/viewer stamps CONVERGE to broker session-table truth, not merely edge-trigger `[REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]` Failure (F-026 stamp-gap, hall-b + the original ball-b):** a picker-created endpoint (`endpoint run` → new) read plain `ONLINE` in the list + picker while genuinely driven — `info.json` `controlled:false` throughout. ROOT (the UPWARD companion to 7.27's downward edge-clear): the broker spawn path's `become_controller` → `stamp_driven_by` → `set_controlled(true)` fires at SPAWN time, but a FRESH endpoint has NO PERCH yet (the adapter binds it after claude boots), s B) REQ-PICKER-CONTROLLED-LOCAL Title: #3 local half: a LOCALLY-controlled endpoint renders CONTROLLED in its own node's picker. display_status() (crates/spt/src/picker/model.rs:415) derives Controlled ONLY from driven_by.is_some(), but driven_by is REMOTE-only by design (KH 7.15) — a locally-controlled endpoint has driven_by=None + controlled=true, and local_rows (data.rs:220) never threads controlled into EndpointRow, so a locally-RC'd endpoint shows plain ONLINE in its own picker (remote rows are fine — gossip stamps controller_node=self, REQ-GOSSIP-CONTROLLED-ANY; the asymmetry is the bug). Fix: EndpointRow gains controlled:bool (local: rec.controlled; remote: controller_node.is_some()); display_status -> Controlled when driven_by.is_some()||controlled; desc pane says 'controlled locally' when the driver is unnamed. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #3. Doc: PAIR 28: REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND <-> REQ-INPUT-CONTROLLER-FENCE ================================================================================ A) REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND Title: DEFERRED EDGE of REQ-HAZARD-CONTROLLER-GAP-RESUME: when a serving controller falls behind the live ring FURTHER than the ring holds (tail - delivered_through > cap_chunks, the ring trim being delivered_through-blind, broker.rs:327-330), the dropped frames have rolled OUT of the ring and an exactly-once re-fetch is IMPOSSIBLE. v0.13.0 DETECTS this (resume floor unchanged across two consecutive resumes) and surfaces a MARKED truncation notice to the operator then ends the attach cleanly — it never silent-skips (a B2 lie) and never spins. FULL graceful handling (a clearly-marked snap-with-data-loss that keeps the operator on the live tail, or a structured truncation record the rc renders distinctly, plus the controller-too-slow + ring-too-small backpressure/sizing policy) is DEFERRED — staging it needs a netsplit / deep-behind harness (the in-process loopback rig keeps up; wedged_viewer's gap is recoverable at ~1492 < 4096). (v0.13.0+) Doc: B) REQ-INPUT-CONTROLLER-FENCE Title: RC-RENDER-TRUTH W2 (ADR-0044 decision 3, hertz same-machine --take split-brain RCA P0-C + scope clarification, doyle seam-verified broker.rs dispatch_input 3920-3935 session-addressed unfenced): broker-enforced input fencing SCOPED TO RC-ORIGIN INPUT — RC Input/Resize bind to the ACTIVE controller lease (or originating broker connection as the N-1 surrogate); commands from a displaced/stale lease are rejected/dropped after replacement. Do NOT globally gate generic KIND_INPUT: shell/system injection legitimately sends InputReq from non-controller connections (Minter::Shell, shellchan seam) — fence keys on an additive controller-ownership token validated only for token-bearing/Minter::Rc requests, or a dedicated guarded RC-input verb; token optional/default-none preserves generic injection exactly. REQUIRED DEFENSE, not optional hardening: this is what makes the at-most-one-input-capable-controller invariant TRUE even when the Displaced notification is delayed or lost (today the displaced window keeps typing into the PTY indefinitely — the field split-brain). Gate: impl — token/verb + lease-bound validation on the RC input path; unit — stale-lease RC input rejected post-replacement, active-lease applies, tokenless generic injection (shell) UNCHANGED, absent-lease N-1 falls back to connection fencing; int — hertz regression steps 5-6 (post-takeover Input+Resize from A mutate NOTHING, from B both apply) + shell injection still lands mid-controlled-session; doc — ADR-0044. Doc: Across DIFFERENT `by` identities the intent split stands: `Control` = Busy, `Take` = loud revoke. Deliberate, documented UX consequence: a second same-node window's plain `rc` now LOUDLY displaces the first (newest viewport wins within one identity) — the pre-W2 behavior was the same replacement done SILENTLY with the loser left interactive and blind; loud + fenced is strictly better on every axis, and `--view` remains the coexistence path. Every ruled invariant holds: at most one input-capable lease, the incumbent always ends terminally, a displaced window can never type, equal-gen recovery s PAIR 29: REQ-DAEMON-1 <-> REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED ================================================================================ A) REQ-DAEMON-1 Title: One per-machine spt-daemon owning all per-machine state Doc: B) REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED Title: SEED (doyle-filed, 2026-07-05, from the REQ-HAZARD-DAEMON-STOP-BARRIER B2 fix): the PRODUCTION seedmap connect callers (put / take / is_running) inherit the SAME interprocess WaitNamedPipeW-forever hazard the stop path just bounded — a Windows named-pipe connect to a name that EXISTS but has NO accepting instance parks in NMPWAIT_WAIT_FOREVER, so a slow / half-dead seed daemon could wedge a live `api seed` / `api listen` / `daemon start`. UPDATE (2026-07-05, doyle reversed the stop-path scope-guard): request_stop's OWN initial connect became load-bearing (a stop-guard re-dialing an already-dying name parked forever, resurrecting the convoy) → it is now bounded via connect_bounded under REQ-HAZARD-DAEMON-STOP-BARRIER (every dial on the STOP path is bounded). REMAINING deferred here = the put / take / is_running production clients. FIX (deferred, needs its own ruling): a shared bounded seed-control connect for those — but a 2s-style cap on a legitimately slow daemon-start connect is a real behavior change (a slow-but-fine start could become a spurious failure), so the timeout + degrade semantics need design first. NOT built — activate when scoped. Doc: PAIR 30: REQ-DAEMON-STDERR-PERSIST <-> REQ-DAEMON-STOP-LIVE-SESSION-WARN ================================================================================ A) REQ-DAEMON-STDERR-PERSIST Title: W3 (LIFECYCLE-TRUTH, observability): broker + brain stderr tee to a rotating size-capped file under SPT_HOME (e.g. 2x5MB), stamped per generation. ROOT: detached daemon nulls stdio -> the 2026-07-06/07 incident window left ZERO logs (both RCAs ran blind; rigs had to recreate everything). KNOWN-HAZARDS note: never inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe. Doc: B) REQ-DAEMON-STOP-LIVE-SESSION-WARN Title: W3 (LIFECYCLE-TRUTH, promoted old follow-wave seed): `daemon stop` with live hosted sessions warns + requires --force (or names the sessions it will kill) instead of silently killing them. Doc: PAIR 31: REQ-HAZARD-RC-EOF <-> REQ-NET-FAMILY-GATE ================================================================================ A) REQ-HAZARD-RC-EOF Title: 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) Doc: B) REQ-NET-FAMILY-GATE Title: spt-core binds only IP families that are actually REACHABLE (ADR-0030), so a half-broken family (e.g. IPv6 whose AAAA resolves but whose path is dead) never silently consumes connection/discovery time. At NetEndpoint::bind (endpoint.rs), probe each family's reachability ONCE and bind only the working ones: dual-stack when both healthy; IPv4-only when IPv6 is dead; IPv6-only when IPv4 is dead (drop the DEAD family — NOT a fixed prefer-IPv4; IPv6-only networks must keep working). Re-evaluated on daemon restart (once-at-bind, no live re-eval in v1). Explicit overrides SPT_DISABLE_IPV6 / SPT_DISABLE_IPV4 force a family off (escape hatch + determinism + testing, mirroring SPT_NTP_SERVER); a forced-off family is never bound regardless of the probe. Reusable beyond join — every spt connection benefits. (next milestone) Doc: Robust WAN subnet join: meet-before-code + per-family bind gate PAIR 32: REQ-DOC-DELIVERY-VOCAB <-> REQ-HAZARD-DEFERRED-DRAIN ================================================================================ A) REQ-DOC-DELIVERY-VOCAB Title: W6 (LIFECYCLE-TRUTH, docs — remaining flynn/perri gaps folded): publish the full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED window semantics / DEFERRED / NO_PERCH), digest --json row schema, api poll auth + MAC-stamp prefix, remaining --json shapes checklist (seed #3). Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies. Doc: the send-outcome vocabulary itself — the closed set of SENT/SENT(WAN)/QUEUED/QUEUED(idle-only)/DEFERRED/NO_PERCH + WAN failure tags with their exact conditions; the JSON-consumer view + digest/poll shapes live in reference/json-shapes.md / the machine-consumer reference: send-outcome vocabulary (canonical home cross-linked to Messaging), the endpoint-digest --json schema, the shell relay MAC-stamped frame prefix + api poll auth, and the full --json shapes catalog (seed #3) B) REQ-HAZARD-DEFERRED-DRAIN Title: Deferred spool rows excluded from the event-stream drain (1.4) Doc: PAIR 33: REQ-DOCS-3 <-> REQ-SEND-REPLYTO-REMOVE ================================================================================ A) REQ-DOCS-3 Title: Diátaxis structure; one canonical way to do X Doc: Diátaxis four-mode separation + one-canonical-way, stated as the corpus's contract How these docs are organized B) REQ-SEND-REPLYTO-REMOVE Title: Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.16.0) Doc: PAIR 34: REQ-DOCS-NO-INTERNAL-CODES <-> REQ-XTASK-SPT-BIN-TARGET-DIR ================================================================================ A) REQ-DOCS-NO-INTERNAL-CODES Title: Public CLI --help (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading --help and ship to GH-Pages. A CI-gated scan (the `xtask check` docs gate) fails on any such token in the GENERATED reference.md (which by construction contains only clap help, so rustdoc `///` on fns/structs is OUT of scope and keeps its REQ/ADR cross-refs). Substance is kept; codes are rewritten to plain language. (v0.13.2 W6) Doc: Public help carries no internal codes.** The public CLI `--help` (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading `--help` and ship to GitHub Pages. The `xtask check` docs gate scans the **generated** reference for these tokens and fails on any hit (regeneration alone keeps drift at zero but would faithfully republish a leak — the scan is what gives the gate teeth). The scan is scoped to the generated reference by construction, so B) REQ-XTASK-SPT-BIN-TARGET-DIR Title: #13 (F026 micro, tooling): xtask `spt_bin()` (crates/xtask/src/main.rs) BUILDS `spt` via cargo (which honors CARGO_TARGET_DIR) but returns a HARDCODED `/target/debug/spt` path — so under a redirected target dir (CI / isolated-gate rigs that set CARGO_TARGET_DIR to a throwaway) the binary lands in `$CARGO_TARGET_DIR/debug` while xtask looks in `/target/debug` -> NotFound -> `xtask check` (docs-drift gate) spuriously fails. Workaround was running `xtask check` with CARGO_TARGET_DIR unset. FIX: a pure `target_debug_dir(root, CARGO_TARGET_DIR)` seam mirroring cargo's resolution — absolute override as-is, relative resolved against `root` (the dir cargo is invoked in), default `/target` — join `debug`; `spt_bin` returns from it. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md. Doc: PAIR 35: REQ-RUN-ID-REUSES-ADAPTER <-> REQ-UPDATE-ONE-SHOT-FINISH ================================================================================ A) REQ-RUN-ID-REUSES-ADAPTER Title: D-1 (REMOTE-TRUTH triage §D-1): `spt endpoint run --id ` with NO --adapter, when names an EXISTING perch, REUSES that perch's recorded info.adapter and runs NON-INTERACTIVELY — instead of always falling to the picker as a create-new prefill (an existing endpoint retyping its own adapter, or being sent to a create-new flow, is the operator wart). ROOT (certain, no design tension): the cli `match (adapter,id)` special-cased only (Some,Some)→cmd_endpoint_run; the catch-all routed EVERY lone --id to crate::picker::run as a create-new prefill, never considering an existing endpoint (cli.rs ~1290). FIX: a PURE resolve_run_target(adapter, id, recorded) over the 4 (adapter?,id?) quadrants — (Some,Some)→Direct{a,id}; (None,Some(id))→ recorded adapter present (info.adapter = adapter-chosen-at-creation, spt-store info.rs:167) → Direct{recorded,id}, absent/no-perch → Picker{None,Some(id)} (today's create-new prefill UNCHANGED); (Some,None)/(None,None)→Picker unchanged. The perch lookup (read_info(resolve_perch_path(id,Infer)).adapter) is INJECTED as a closure so the router is pure + testable without a perch on disk; resume threads into BOTH Direct paths. Red-first: (None,Some(id),recorded=Some) → Direct (pre-fix this routed to Picker create-new). int (live run --id on an existing perch reuses its recorded adapter non-interactively) deferred to the rig. Doc: B) REQ-UPDATE-ONE-SHOT-FINISH Title: W3 (LIFECYCLE-TRUTH): update apply works daemonless and one command finishes the cycle. ROOT (operator wart): update fetch/apply run ensure_daemon_announced (cli.rs:4386) -> on a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain pair + a manual bounce. FIX: apply works daemonless (swap + record, next start runs new bytes); `update apply --finish` (name subject to docs-token gate) completes the cycle: swap -> brain cycle -> broker restart onto new bytes (rides REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so the restart is not a massacre). CLI change -> xtask docs gen, no internal codes in clap ///. Doc: PAIR 36: REQ-DAEMON-5 <-> REQ-EP-4 ================================================================================ A) REQ-DAEMON-5 Title: 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) Doc: B) REQ-EP-4 Title: PresenceChannel broker endpoint (seam day-one) Doc: PAIR 37: REQ-DAEMON-6 <-> REQ-SPAWN-COLLISION-GUARD-LIVE-DUP ================================================================================ A) REQ-DAEMON-6 Title: 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. Doc: B) REQ-SPAWN-COLLISION-GUARD-LIVE-DUP Title: W4 (LIFECYCLE-TRUTH): single-flight wake per endpoint — the WAKE/RESUME respawn seam must not launch twice for one wake. ROOT (perri parentage + recovered filing): one wake processed TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume ` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. DAMAGE: duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a fresh /clear rotation -> injects routed to the contended record and lost). FIX: single-flight wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim. Int: two concurrent wake requests -> exactly one launch tree. Doc: PAIR 38: REQ-EFFECTIVE-INSTANCE-STATE <-> REQ-LIST-JSON-LIVENESS-PARITY ================================================================================ A) REQ-EFFECTIVE-INSTANCE-STATE Title: A-1 (REMOTE-TRUTH triage §A + ADR-0033 §Decision): the effective instance state of a perch is DERIVED through ONE shared function — liveness discriminates warm/cold, stored rest intent refines within warm, absent intent NEVER defaults active. ROOT (certain): resting::apply_event derived its `from` off the stored rest_state field ALONE (resting.rs:225, `unwrap_or(RestState::Active)`) — a cold perch (offline) with no intent answered `from=Active`, so a Wake event found it 'already in target state' and returned Ok(None) = the field NO_EDGE-on-a-definitely-suspended-endpoint bug (the banked F-028 rest_state-void seed). advertised_status (registryhost.rs:821) ALREADY derived correctly (is_perch_alive→intent-refined / is_perch_unbound→Dormant / cold→Suspended) — the two readers disagreed. FIX (Q1 shared derivation, hazard-class): a pure `effective_rest_state(alive, unbound, intent) -> RestState` mirroring advertised_status, consumed by BOTH advertised_status (mapped RestState→Status, behavior identical) AND apply_event's `from` (real is_perch_alive/is_perch_unbound reads); void + cold ⇒ Suspended. Bonus: kills the spurious active→suspend echo a cold+void perch used to fire (on_rest_edge on a dead driver). Red-first: perch status=offline + no rest_state → daemon_rest_event(Wake) yields from=Suspended→to=Active EdgeReport, not Ok(None). Doc: 7.32 The effective resting state MUST be derived through ONE shared liveness-aware function — a stored-intent-alone read lies about cold perches `[REQ-EFFECTIVE-INSTANCE-STATE]` Failure (paid-for, field evidence):** two rest-state readers derived the effective instance state independently. `registryhost::advertised_status` read it liveness-aware (cold ⇒ Suspended); `resting::apply_event` derived its `from` off the stored `rest_state` intent field ALONE (`unwrap_or(RestState::Active)`). A cold (offline) perch with no resting intent therefore looked **Active** to `apply_event` — so a `Wake` even B) REQ-LIST-JSON-LIVENESS-PARITY Title: GATEWAY-LIVENESS (flynn field bug 2026-07-09, RCA reader-divergence root): `spt endpoint list` (human) and `endpoint list --json` MUST report an IDENTICAL status for a locally-hosted endpoint — especially a pid-alive, status-ABSENT gateway (no psyche_init). ROOT (todlando RCA STEP-1, doyle-verified): the --json builder (crates/spt/src/cli.rs cmd_endpoint_list) emits each subnet row's status straight from resource_projection (spt-net registry.rs:566, passes instance.status through verbatim :592 — the persisted WAN snapshot, a lagged gossip that can carry a stale/crash-time Suspended) and NEVER applies the self-owned reconcile the human/picker path applies (reconcile_self_owned, crates/spt/src/picker/data.rs:160 via gather_endpoints :112). So a pid-alive self-owned gateway reads Suspended on --json but ONLINE on human (roster::enumerate spt/src/roster.rs:38 -> is_perch_alive pid-fallback spt-store/liveness.rs:136); the adapter suspend-poll (parse_endpoint_status over endpoint list --json --show-all) reads the divergent --json status -> self-suspends a pid-alive gateway. Candidates REFUTED: resource_projection does NOT re-derive liveness (copies instance.status, only skips !routable :582); render is a faithful {:?}. advertised_status (registryhost.rs:822) DOES compute Active via pid-fallback (live advertise fine) — the bug is the READER showing the un-reconciled snapshot. FIX (doyle-ruled): apply the self-owned reconcile in the cli --json builder (SAME spt/picker consumer layer as gather_endpoints, NOT lifted into resource_projection which inverts the dep spt-net->perch), reading LOCAL perch truth (is_perch_alive/unbound -> Active/Offline, mirroring the picker's local_rows). Do NOT force status=online (DEFECT B latent — seed-#5 orphan-listener false-ONLINE risk). Gate: a pid-alive locally-hosted gateway (status-absent, no psyche_init) reads the SAME non-Suspended status on human AND --json. Field-verify flynn (mobile-gw). Kin REQ-PICKER-3 + REQ-PRESENCE-LIVENESS-TRUTH + seed-#5 orphan-listener false-ONLINE. Doc: PAIR 39: REQ-ENDPOINT-CYCLE-HONEST <-> REQ-HAZARD-ATTACH-WEDGE ================================================================================ A) REQ-ENDPOINT-CYCLE-HONEST Title: REGISTRY-LIFECYCLE W3 (ADR-0041 decision 6, operator deployah stop/run wedge): cycle verbs share ONE liveness authority — the ALREADY_LIVE dup-guard liveness-probes the claimed session client tree before refusing (dead tree means reap + respawn honestly, never a refusal citing a zombie); the shutdown state machine consults the same source so is-it-live has one answer (no ALREADY_LIVE / list-OFFLINE / shutdown-NO_EDGE three-way contradiction on the same endpoint). Gate: impl — probing dup-guard + unified authority; unit — dead-tree claim probes and reaps, live claim still refuses; int — controlled zombie (killed client tree, surviving hosted record) leads to endpoint run succeeding honestly end-to-end; doc — ADR-0041. Doc: 1. **Online is earned, not declared.** A creator may stamp `status=online` only from actual persisted state + hosting authority — never from manifest capability alone. Legacy hybrid rows self-heal at reconcile, but only after a SUCCESSFUL broker query: a broker failure is never interpreted as an empty session set (no mass-offline on a hiccup). 2. **Control cleanup splits from offline classification.** Reconcile clears `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth — regardless of state or controllability — while offline classification keeps its narrow gate B) REQ-HAZARD-ATTACH-WEDGE Title: 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) Doc: PAIR 40: REQ-ENDPOINT-LIST-PALETTE <-> REQ-TRANSLATE-COMMIT-MISS-TOLERANCE ================================================================================ A) REQ-ENDPOINT-LIST-PALETTE Title: Bugs #11 + #15 (display): spt endpoint list renders status as plain text while the picker turns the same ResourceRow into the W5 colored EpDisplay palette. Fix: extract one shared ResourceRow-to-EpDisplay builder + make the picker display enums/helpers public, and have endpoint list render the same colored status squares (via helpfmt stdout_color, not ratatui Span). This also fixes #15 — a lone warm detached instance renders as its online flavor (Dormant maps to online) instead of leaking the bare word Dormant through the text-only list (no resting.rs/CONTEXT model change; operator ruling display-only). Couples REQ-PICKER-NODE-GROUPING (both edit subnet_rows — sequence the shared-builder extraction first). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #11/#15. Doc: B) REQ-TRANSLATE-COMMIT-MISS-TOLERANCE Title: C-1 (F029, B6 ROOT — rescope of REQ-TRANSLATE-BINARY-LIVENESS-DECAY, now PINNED): at a checkpoint clear boundary the clear-only inject drives `/clear`; its `{commit}` is never observed within INJECT_COMMIT_DEADLINE (5s, broker.rs:158) so the inject worker FAULTS + TERMINATES a HEALTHY translate binary (broker.rs respool_and_fault + return) and by ADR-0022 design NEVER respawns → every subsequent force-native reports delivered=false ('no live translation binary', cli.rs:5046) = B6's exact field signature; the v0.12.0 checkpoint post-clear WAKE dies with the terminated binary + the fire-and-forget FIRE in the dead window. NOT a race — deterministic at every checkpoint-armed boundary (perri captured-stderr proof: TRANSLATION_FAULT on the F-019 unread daemon-stderr channel). FIX (REVISED, supersedes terminate-then-respawn): miss != fault — preserve the binary; the watchdog's job is ANTI-STALL (release the operator floor), not execution-verification. See addendum C-1 + ADR-0022 amendment. Doc: Amendment (2026-07-03, F029 C-1) — a commit-miss is NOT a fault; real faults respawn PAIR 41: REQ-ENDPOINT-LIST-REST-FILTER <-> REQ-SPAWN-COLLISION-GUARD-LIVE-DUP ================================================================================ A) REQ-ENDPOINT-LIST-REST-FILTER Title: spt endpoint list hides SUSPENDED instances by default; a new --show-all flag reveals them. Status-first row ordering with fixed precedence ONLINE > CONTROLLED > UNBOUND > SUSPENDED (when shown) > corrupt last, alphabetical by id within each band. Two invariants: (1) CORRUPT rows ALWAYS render regardless of filters — corrupt is a record condition demanding operator action (purge/re-mint), not resting clutter; hiding it would re-create counter-39 bug #3 (cross-ref REQ-HAZARD-CORRUPT-PERCH-COHERENCE, CONTEXT.md instance-state _Also avoid_); (2) the per-node Total line DISCLOSES the filter — 'Total: N (+M suspended hidden)' — so nothing silently vanishes. Registry-Offline rows stay excluded by projection law (resource_projection skips unroutable; unchanged). Grill-with-docs ruling 2026-07-02 (operator + doyle). Doc: B) REQ-SPAWN-COLLISION-GUARD-LIVE-DUP Title: W4 (LIFECYCLE-TRUTH): single-flight wake per endpoint — the WAKE/RESUME respawn seam must not launch twice for one wake. ROOT (perri parentage + recovered filing): one wake processed TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume ` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. DAMAGE: duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a fresh /clear rotation -> injects routed to the contended record and lost). FIX: single-flight wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim. Int: two concurrent wake requests -> exactly one launch tree. Doc: PAIR 42: REQ-ENDPOINT-ONLINE-TRUTH <-> REQ-RESIDENT-SERVICE ================================================================================ A) REQ-ENDPOINT-ONLINE-TRUTH Title: REGISTRY-LIFECYCLE W2 (ADR-0041 decisions 1+2, emphasys C1 P0): ONLINE is earned, not declared — cmd_listen stamps status=online only from actual persisted state + hosting authority, never from manifest psyche_init capability alone (no more ready_agent/controllable=false hybrid rows born online-authoritative); livehost reconcile SPLITS control cleanup (clear controlled/driven_by/viewer_count for EVERY endpoint absent from session truth, regardless of state/controllable) from offline classification (live_agent+controllable gate unchanged); legacy hybrid rows self-heal after a SUCCESSFUL broker query only (broker failure is never interpreted as an empty session set); terminal signoff/owner-loss = atomic CAS-guarded offline + ready/address removal WITHOUT overloading soft api session-end (/clear preserves the live listener). Gate: impl — creator gate + reconcile split + self-heal + terminal path; unit — creator refuses capability-only online, cleanup clears stamps on state-quirk rows, broker-failure never mass-offlines; int — dead-PID hybrid row does NOT survive a reconcile cycle (the immortal-row regression); doc — ADR-0041. Doc: 1. **Online is earned, not declared.** A creator may stamp `status=online` only from actual persisted state + hosting authority — never from manifest capability alone. Legacy hybrid rows self-heal at reconcile, but only after a SUCCESSFUL broker query: a broker failure is never interpreted as an empty session set (no mass-offline on a hiccup). 2. **Control cleanup splits from offline classification.** Reconcile clears `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth — regardless of state or controllability — while offline classification keeps its narrow gate B) REQ-RESIDENT-SERVICE Title: ResidentService substrate (ADR-0049, design ratified 2026-07-26): a daemon-supervised binary an adapter declares via a `[service]` manifest section — core-owned from birth, NO perch/identity/address. SPAWN: the daemon launches it job-neutrally (detached_no_inherit + the cold-start ladder posture), so it is never a shell's child (`/T` tree-kill cannot reach it; the shell-descendant hazard class of REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE never arises) and never inside a launching terminal's Job Object (the REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE service half closes by construction). START TRIGGER declared in the manifest: start = 'boot' or start = 'bind'; supervised identically once running, with the wake-watcher scaffolding (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) minus the offline-only flip. 'boot' is DESIRED-STATE-RUNNING, not an event: the supervisor reconciles a boot service toward running at EVERY opportunity — daemon boot, ADAPTER REGISTRATION while the daemon is live (operator addition 2026-07-26: installing or registering an adapter whose manifest declares a boot service starts it THEN — spt itself is never restarted to bring a new adapter's service up), update-hold release, and first shell bind as the defensive ensure. 'bind' starts only at the adapter's first shell bind. CARDINALITY: one supervised instance per registered adapter-option `[:profile]` (consumer-confirmed as COHERENT TARGET SHAPE — flynn's precision, 2026-07-26: not exercisable by the first consumer until per-option config dirs exist; alchemy today has one config dir and daemon.toml carries exactly one guild_id); the adapter may keep its own kernel file lock as a private double-start guard — core neither reads nor depends on it. PER-OPTION IDENTITY IS THREADED (flynn's gap, accepted): the supervisor passes the adapter-option name and the per-option runtime dir into the service's spawn environment, so an adapter can scope its private guard AND its config per option — the mechanism that makes two-options-two-services deliverable rather than merely permitted. Without it, two options resolving one adapter config dir produce the silent flap flynn derived: instance two exits immediately on the kernel lock, core sees only start-then-die, and crash-relaunch backoff is CORRECT behavior against that observation — two correct components disagreeing about the unit. UPDATE IS A FIRST-CLASS SUPERVISOR OPERATION WITH AN EXPLICIT HOLD: quiesce -> hold (stopped and NEVER relaunched while held) -> bits swap -> start new bits -> release; adapter update-apply performs this ordered operation; crash-relaunch with backoff applies ONLY when not held. The forcing case is structural, not advisory (flynn, argued against their own convenience): an eager relaunch during a swap re-pins the OLD exe mid-deploy (Windows exe lock), converting a diagnosable os-error-5 into an unwinnable race — if delivery must sequence, the hold ships FIRST and a dead service stays dead until told otherwise (the STALE-ONLINE no-spontaneous-relaunch ruling, same reason, one layer down). FAST-EXIT IS A CONFIGURATION FAULT, NOT A CRASH: consecutive immediate exits (exit within a startup threshold, N in a row) trip the give-up latch EARLY with a distinct loud STARTUP_FAULT diagnostic carrying the captured early stderr — a double-start lock conflict then reads as the configuration fault it is, never as a silently flapping service ground through backoff. QUIESCE IS COOPERATIVE EXIT + DEADLINE: the supervisor places a stop-request marker (a file in the service's runtime dir — polling services observe it on their next cycle; no inbox exists or is added); the service exits WHEN SAFE and the kernel-observed exit IS the ack — 'not ready' is expressed by not-yet-exiting, so no busy record exists to go stale in either direction; a manifest-declared grace deadline (default ~30s) bounds the wait, then force-kill. Delay possible, veto never. An OPTIONAL advisory status line may surface in service status display — never consulted for decisions. LIVENESS IS DERIVED, NEVER RECORDED: the supervisor is the parent and holds the child handle (exit is kernel-observed); no supervisor-maintained running-record exists (the v0.43.0 stale-online lesson applied one layer down — flynn's condition, structural here). Any status/version identity surface keeps the locked-file split lesson: never require reading a file the service holds an exclusive OS lock on (Windows). CLI INVOCATION CAPABILITY (consumer-blocking, non-negotiable per flynn): the supervisor threads the environment so the service can invoke the spt CLI (`spt send` et al., identityless cli@node from-label, durable spooling per ADR-0002) — if a supervised Hub cannot shell out to spt send, node-wide Watch delivery dies silently. ADDRESSING: none — a service needing a two-way agent-facing surface has one at its adapter's endpoint/shell layer (the alchemy layering argument that re-scoped ADR-0023's faceless-service rejection); AlwaysOnEndpoint (REQ-EP-8) = this substrate + the addressable front. Gate at activation (all legs against a MOCK service adapter — the gate never depends on the first consumer being the boot specimen): int — a manifest [service start='boot'] binary rises with the daemon job-neutrally, a registration of that manifest against an ALREADY-LIVE daemon starts the service without any restart, survives a shell teardown of the same adapter (tree-kill does not reach it), a held update swaps bits with zero relaunch races (hold observed under a concurrent crash), quiesce marker -> cooperative exit within grace, deadline -> force-kill on a wedged mock, a mock that exits instantly N consecutive times surfaces STARTUP_FAULT (not a backoff flap), and the service successfully invokes spt send from its supervised environment. ACTIVATED FOR W1 2026-07-26 WITH THE FOLLOWING BUILD RULINGS FOLDED IN (doyle; constraints live in the artifact, not the dispatch thread). VERB SURFACE: the operator-facing verbs are `spt adapter service list` (all registered options + derived state) and `spt adapter service status `, nested under the ADAPTER group — NOT a bare `spt service`. Reason: 'service' is ALREADY public surface carrying an unrelated meaning — the platform daemon-service abstraction (REQ-DAEMON-6/-8, `crates/spt-daemon/src/service.rs`: the systemd user unit vs the Windows at-logon task) surfaces in `spt daemon` help as 'registered OS service' / 'managed service' / 'the managing service label'. Ownership-scoping separates the two meanings permanently: the OS-service is the DAEMON's and lives under `spt daemon`; the resident service is the ADAPTER's and lives under `spt adapter`. ADR-0049's 'service status display' means `spt adapter service status`; the W1 PR carries a one-line ADR errata note. GIVE-UP LATCH SCOPE: the latch suppresses relaunch grinding until something plausibly changed — it is NOT a durable verdict. Cleared by exactly three events: (1) DAEMON BOOT — desired-state-running enumerates boot as a reconcile opportunity with NO latch exception, and the re-trip is bounded (N fast exits) and LOUD (STARTUP_FAULT re-fires each boot); an in-memory per-daemon-lifetime latch is an acceptable implementation, and if the durable-marker scaffolding is reused then boot clears the marker; (2) ADAPTER RE-REGISTRATION — declared intent that manifest/config changed, reconciling immediately; (3) UPDATE-HOLD RELEASE — new bits invalidate the fault evidence. NOT cleared by the first-shell-bind ensure: a bind changes nothing about the service's config, so the bind-time reconcile SKIPS latched services — otherwise ordinary shell use converts the latch into the very flap it exists to stop. The REJECTED alternative is recorded deliberately: a durable latch with explicit-clear-only leaves an operator's already-fixed config fault sitting behind a service that stays dead and quiet forever — loud-bounded beats quiet-permanent (the heal-assigns-owner-never-closes shape one layer down). SANCTIONED BUT NOT W1-REQUIRED: `spt adapter service restart ` as the explicit manual clear+reconcile — take it into W1 only if cheap once the verb group exists; the three automatic clears ARE the requirement. REGISTRATION-TIME START IS A WIRE OP: `registry::register` runs in the CLI PROCESS (`crates/spt/src/cli.rs`, the adapter-add and adapter-update call sites), so it cannot itself start anything in the daemon — registration-starts-the-service structurally requires ONE new adapter-scoped daemon IPC op, semantically `AdapterServiceReconcile { adapter }` (exact spelling matched to house op style where it lands), which the CLI calls after a SUCCESSFUL register on BOTH paths when the daemon is reachable. The daemon handler runs THE SAME reconcile code path as boot / hold-release / bind — ONE reconcile function taking an opportunity discriminant, never a second start authority. The response is a per-option outcome list (started / already-running / held / latched / bind-deferred / startup-fault) so the CLI prints honest per-option text. Daemon NOT reachable: registration STILL SUCCEEDS — never a refusal — and the CLI prints a REQUIRED notice that the service is declared, the daemon is not running, and it will come up at the next daemon boot. That notice is CONTRACT, not courtesy. RUNTIME-DIR ENCODING: cardinality is per adapter-option, so the option name becomes a PATH component and `:` is illegal in a Windows path. Core NEVER uses the raw option string as a path component — every construction site goes through ONE shared encoder, and that encoding MUST BE INJECTIVE (a lossless escape, never a strip/replace that can collide). Two distinct options mapping to one runtime dir means two services sharing a quiesce-marker namespace: the silent flap one layer down. The unit gate MUST include a collision-adversarial pair (e.g. if `:` maps to `_`, then `a:b` and `a_b` must remain distinct). MODULE PLACEMENT: the supervisor lands as `servicehost.rs` (the established `*host` convention — shellhost, harnesshost, linkhost, applyhost); `service.rs` is untouched and BOTH module headers cross-reference the other meaning of 'service'. INSTALL-DIR RESOLUTION RIDES W1 (operator-requested via flynn, ruled in after code verification): the `[service]` spawn resolves its binary through the EXISTING REQ-INSTALL-11 helper — the same primitive, NO parallel resolution path — and the same wiring lands at the two shell-family fill sites, which today resolve NEITHER the install-dir program token NOR `{adapter_dir}`. Without it a `--release`-installed shell adapter registers but cannot spawn (bare token → os error 2; `{adapter_dir}/x` → 'no value for substitution key'), released shell adapters need a hand-maintained per-node manifest, `spt adapter update` on them is a permanent no-op, and THIS req's hold/swap/start ceremony would be exercisable by MOCK ONLY — never by the named first consumer. Site census discharged BEFORE build (authoritative grep, cfg(test) excluded, accepted by doyle): the production template-fill sites are `shellhost.rs::fill_spawn_command` and `shellwake.rs::fill_wake_command` (both targets — threading not shape, since the wake caller already holds `AdapterRecord.source_dir`, which IS the install dir), plus `harnesshost.rs` session `role.command`, which is OUT OF SCOPE and already resolves correctly through `resolve_program_in_dir`. Scope guard: shell spawn/wake + `[service]` ONLY — this does NOT expand into the `[session.self]`/`[history]` follow-on sites. FAULT COUNTERS ARE TWO, NOT ONE (ruled 2026-07-26 after the builder surfaced the reading): (1) the FAST-EXIT counter increments ONLY on exits whose uptime is UNDER the startup threshold, and RESETS the moment any run EXCEEDS that threshold. Without the reset the latch mislabels slow-crash decay as a configuration fault — the latch lying about cause, which is worse than no latch. STARTUP_FAULT is reserved for THIS path alone. (2) The ORDINARY consecutive-crash give-up (the shell wake-watcher scaffolding's `give_up_after` = 6, deliberately UPTIME-BLIND) sits BEHIND it UNCHANGED in W1 and keeps its existing diagnostic label — no silent behavior fork from the shell watcher. (3) BOTH counters reset on the latch-clear events above (daemon boot, adapter re-registration, update-hold release): a clear that left either counter primed would relatch on the first post-clear crash and thereby defeat the clear. (4) NOTED-OPEN, deliberately NOT W1 and NOT a promise: the ordinary counter's uptime-blindness means a service that crashes once a day gives up after six days and then stays down until a clear event. Whether that decay behavior is right for SERVICES (as opposed to the shell watchers it was designed for) is a future ruling; it is recorded here as open so the next builder inherits the question rather than rediscovering it in the field. ORPHAN ADOPTION IS IMAGE-VERIFIED, AND ITS PLATFORM GAP IS RECORDED-OPEN (ruled 2026-07-26 after the builder surfaced the trade): a fresh daemon kills a dead daemon's parked orphan ONLY by path-verified identity — a live pid whose image path cannot be READ classifies `Unverifiable` and BLOCKS adoption, i.e. the start refuses loudly rather than proceeding. Loud-blocked over quietly-double-started is this design's whole posture (a bare-pid kill is the recycled-pid class, and a double-start is the silent flap the cardinality rule exists to prevent). RECORDED-OPEN consequence, NOT debt owed by W1: the image oracle is `/proc` on unix, so a unix WITHOUT `/proc` (macOS/BSD) would block on every live orphan until that pid dies. This is theoretical for every platform we ship — win, linux-gnu, musl — and no macOS/BSD asset exists; a future builder adding one inherits the question here rather than rediscovering it in the field. SUPERVISOR PLACEMENT IS BROKER-SIDE (ruled 2026-07-26): the supervised set, its boot sweep and the reconcile control socket live in the BROKER process, beside the digest/drive/tunnel hubs — NOT in the restartable brain child that hosts shellwake. Two reasons, both structural. (a) A supervisor owns LIVE CHILD HANDLES and, from the update ceremony on, an in-memory HOLD: that is a daemon-lifetime continuity resource, which is the ADR-0018 Q2/Q5 broker-side test; the Q5 exception that put shellwake in the brain reads 'a pure disk-reconciler' and this is not one. (b) A brain restart is the ROUTINE UPDATE PATH (StartReason::Update exists precisely for it), so brain-hosting would bounce every resident service through the orphan-adoption path with no quiesce, no grace and no hold — the ungoverned bounce this req's ordered update operation exists to replace — and would lose the hold mid-swap. Broker-hosting is also what makes the wire op possible at all: all control sockets are broker-served because a CLI cannot reach brain memory (stated in drivehub.rs's module header and obeyed by every hub). NO PERIODIC SWEEP — CHOSEN, NOT OMITTED (ruled 2026-07-26): the supervisor host runs the boot sweep and then parks; there is deliberately no timer re-sweeping on a cadence. The four ruled opportunities are all EVENTS, each with a caller that enters the one reconcile function directly, so a timer would be a FIFTH start authority nobody ruled in — and its only distinctive work would be silently healing a failed registration nudge, converting a diagnosable defect into invisible behavior. The REQUIRED daemon-unreachable notice is the honest answer to that case; machinery that papers over its own failure class is refused (the same instrument-soundness razor as the rest of this design). A SUPERVISOR WHOSE DECLARATION DISAPPEARS RECONCILES TOWARD STOPPED (ruled into W1 2026-07-26): every sweep runs a STOP side before its start side — an option whose adapter is soft-deregistered, hard-removed, or whose manifest no longer declares a [service] is torn down through the handle that names its child. This is the symmetric half of desired-state-running, not an addition to it: without it a deregistered adapter's binary outlives its own registration until the daemon dies, which is exactly the ungoverned-lifetime shape this req abolishes. TREE TEARDOWN ON EVERY SUPERVISOR-INITIATED KILL (ruled 2026-07-26): the force-kill deadline is where the unconditional-kill promise is WRITTEN, but it is not the boundary of the problem — a supervised service's descendants are torn down on every kill the supervisor initiates, including daemon-shutdown stop_all and the stop-side sweep teardown. Descendants outliving THOSE paths are strictly worse off than ones outliving a force-kill, because the next daemon's orphan sweep is structurally blind to them: it knows one parked pid and holds no handle to anything below it. Windows reaches the tree through a SUPERVISOR-OWNED Job Object assigned at birth (CREATE_SUSPENDED -> AssignProcessToJobObject -> resume, so no descendant is ever spawned outside the job; KILL_ON_JOB_CLOSE deliberately OFF, so a dying supervisor is never an unannounced service outage); unix through the process group setsid already establishes. Job-NEUTRALITY is not contradicted: that invariant governs OTHER people's jobs reaching our processes, which CREATE_BREAKAWAY_FROM_JOB still handles at birth. A job the OS refuses is a LOUD DEGRADE, never a refusal to start: the process still dies on demand and only its descendants become unreachable, which is exactly where this path stood before the job existed — refusing the spawn would convert a bounded descendant leak into a total outage over a failure in an OS facility rather than in anything the adapter declared. THE STATUS SURFACE IS DAEMON-ANSWERED OR IT SAYS NOTHING (ruled 2026-07-26, the leg-D companion of the tree-teardown ruling): the CLI NEVER derives service state from the pid file. Either the daemon answers `spt adapter service list|status` over the control socket, or the CLI prints that the daemon is not running — verbatim, and with no fallback read. The pid file is a KILL HANDLE for the NEXT daemon, and reading it as liveness in a display surface is the v0.43.0 STALE-ONLINE class one layer down: a record answering a question the record cannot know. Three properties follow and are requirements, not implementation taste. (a) The status op is a PROJECTION over the live supervised set — the supervision threads, the hold flags and the stand-down records — plus the registry; it starts, stops, holds and kills nothing, because a diagnostic that converges the thing it measures is a start authority wearing a diagnostic's clothes. (b) A row reports its EVIDENCE: a latch surfaces with the captured startup output behind it, since a fault reported without its cause is the instrument failing at its one job. (c) An option SUPERVISED WITHOUT A DECLARATION behind it is reported as exactly that rather than hidden — with no periodic sweep, a deregistered adapter's supervisor lives until the next opportunity's stop side reaches it, and that window is precisely when an operator asks what is running. THE ADVISORY STATUS LINE IS DISPLAY-ONLY AND BOUNDED: the service may write one line into its runtime dir (named in docs/MANIFEST.md beside the stop-request marker, so it is implementable); core reads the FIRST line under a byte cap, treats an unreadable file as simply no advisory (the locked-file split rule holds), and consults it for NO decision — core deciding on it would put a least-trusted binary's self-report in the control path, and a service that stopped updating it would silently become whatever it last claimed. WIRE LENIENCY IS PLACED, NOT SPRINKLED: fields a reader BRANCHES on stay typed with a `#[serde(other)]` unknown arm (KH-2.3), while a field only ever ECHOED carries the daemon's label verbatim — re-deriving a lenient copy of the manifest's validated `start` vocabulary for the wire would weaken the one place strictness matters (registration). An op an older daemon predates HANGS UP rather than reading the request and answering nothing: a server that silently ignores an unknown kind leaves the caller blocked on a reply that never comes, so an older daemon would WEDGE a newer CLI instead of failing it. Doc: `command`** — an **opaque** command string (program token plus args), like every other command seam. Its program token resolves against the adapter install dir** before PATH (REQ-INSTALL-11), and args support adapter-static `{adapter_dir}` / `{adapter_name}` substitution only. Must be non-empty: a declared service means spt-core owns and supervises a process. `start`** — **required**, no default. `"boot"` is **desired-state-running, not an event**: the supervisor reconciles the service toward running at daemon boot, at **adapter registration against a live daemon** (installing or registering a PAIR 43: REQ-ENDPOINT-TEARDOWN-AUTHORITY <-> REQ-PSYCHE-LEGACY-RESIDENT-SWEEP ================================================================================ A) REQ-ENDPOINT-TEARDOWN-AUTHORITY Title: TEARDOWN-AUTHORITY W1 (ADR-0045; two hertz field RCAs 2026-07-19, doyle code-verified + ruled): ONE shared topology-aware broker-teardown primitive behind BOTH `endpoint shutdown` and `endpoint stop`. Today both verbs stamp state they never cause: cmd_shutdown (cli.rs:3979) = remove ready marker + cmd_rest(Suspend) — the Suspend edge (resting.rs daemon_rest_event_with_liveness -> apply_event -> cascade_shells_on_edge -> advertise) fires echo + shell cascade + advertise and NEVER touches a session, even though the verb already probed broker-session truth (cli.rs:3797-3804 has_live_session_honest) to force from=alive; cmd_stop (cli.rs:7071) = marker + unregister_address + terminal_normalize + advertise, whose own comment calls it a DEFINITIVE death observation it fabricates. Field: broker retains the whole subtree (adapter -> node -> harness -> nested resumed harness + MCP children; the surviving `spt api listen` is a DESCENDANT, so a direct-child kill misses it). PRIMITIVE (ordered, ADR-0045 decisions 1/6/7/9): resolve broker SessionInfo -> dedicated sid/endpoint-keyed broker kill claiming NO controller (NOT Brain::attach()+kill_session(), brain.rs:622 require_session() = controller theft under the ADR-0044 lease ladder; idempotent — unknown session is benign success) -> reap the whole DESCENDANT subtree (reuse spt_store::proc.rs kill-root+process_descendants, do not mint a second tree-walk) -> await broker-row removal under a bound -> ONLY THEN stamp + unregister + advertise. Stamp-after-reap is load-bearing: field-observed post-stop info.json read status=offline BUT rest_state=active + controlled=true because the SURVIVING host RE-BOUND after the CAS-less terminal_normalize(path, None) — no hardening of the write fixes this, only the reap makes the stamp true (shutdown passes Some(sid) for the concurrent-newer-bind CAS; stop's unconditional posture stays, it is the operator's own definitive command). TOPOLOGY SPLIT (decision 2): gated controllable==Some(true) (ADR-0041 hosting authority — the SAME predicate cmd_bind's online-earn and the livehost reconcile use); harness-hosted/external keeps marker+address+status-only behavior (CONTEXT.md:39 — core spawned nothing, holds nothing) and the misleading cmd_shutdown doc comment claiming marker-removal stops the listener is corrected to name its topology. VERB DIFFERENCES ARE ONLY ceremony + resulting intent (decision 3): shutdown = echo commune + shell cascade FIRST then teardown, result rest_state=suspended (wrap the EXISTING rest edge — apply_event already echoes before the flip per KH 3.3; do NOT reimplement the ordering); stop = NO ceremony, result terminal_normalize. TIMEOUT (decision 7): do NOT stamp cold over a survivor — exit non-zero, and because stop is the last rung with no in-band escalation behind it its failure line names the surviving ROOT PID + the scoped-kill remedy. reconcile_hosted_liveness stays the partial-failure catch-up net, not duplicated. Precedent: REQ-HAZARD-DAEMON-STOP-REAP already ruled this reap principle at DAEMON scope; this is the ENDPOINT scope it was never extended to. Gate: doc — ADR-0045 + CONTEXT.md amendments (190 record-vs-process axis, 640 verb semantics, 647 stop echo exception) + KNOWN-HAZARDS entry; impl — the shared primitive + both verb call sites + topology gate + corrected comments; unit — topology routing table (controllable=true -> teardown, harness-hosted -> marker/address/status only), verb-difference table (ceremony + resulting intent), timeout posture (no cold stamp on survivor, failure line carries root pid), REWRITE shutdown_soft_stops_and_suspends (it asserts marker+intent only = it asserts the bug); int — real broker-hosted endpoint: after each verb, no broker session row + process subtree gone INCLUDING the descendant `spt api listen` + perch ready=false/alive=false/address unregistered/unbound=false + effective and advertised state correct + (shutdown) echo/cascade ran BEFORE the kill + wake/resume works from the recorded sid under the recorded adapter. Doc: 7.49 A teardown verb never stamps a terminal or resting state it has not caused — and two individually-correct verbs must not compose into a lifecycle dead end `[REQ-HAZARD-TEARDOWN-DEADEND]` Failure (paid-for, two hertz field RCAs 2026-07-19, both doyle code-verified the same day; the second hit doyle's OWN live production endpoint):** `endpoint shutdown` reported `Active -> Suspended` and `endpoint stop` reported `STOPPED` while the broker session, its harness child, and a descendant `spt api listen` all kept running — the verbs only removed the ready marker, ran the Suspend rest edge / unre B) REQ-PSYCHE-LEGACY-RESIDENT-SWEEP Title: W5 (F030; doyle+perri 2026-07-04): a dirty daemon upgrade from <=v0.24.0 strands a RESIDENT psyche wrapper the OLD daemon spawned — and F-030 W4's nested-`ready` resolution fix CONVERTED that wrapper's accidental self-reap into a permanent HANG. The pre-W3 wrapper's only spt IPC is `spt ready -psyche --once` (BLOCKING, no internal timeout); pre-W4 that hit READY_FAIL on a multi-subnet home → the wrapper exit-4'd (accidental reap). Post-W4 the nested id resolves cleanly → the wrapper REGISTERS then BLOCKS FOREVER on its first post-upgrade poll: no exit, no psyche_host_error, no CPU (KH 2.6 invisible-loop class, one level up). Post-W3 core has no residency machinery to reap it. FIX: a ONE-SHOT legacy-resident sweep at BRAIN START (never per-reconcile/periodic — burying residency-era machinery, not resurrecting it). GUARD = adapter-AGNOSTIC (glue-model): resurrect the retired reap_orphan_psyches LOGIC — for each self-perch live-agent id derive `-psyche` and kill iff (a) exe basename == the adapter's MANIFEST-declared psyche program (normalize_basename, never a hardcoded adapter name) AND (b) cmdline contains the id marker `-psyche` AND (c) pid alive; any unreadable signal → DECLINE + loud log (fail-safe-decline, positive-match-only; infra never-kill inside the sweep). FRATRICIDE is closed by TIMING (perri-confirmed from the owning side): the ephemeral shim is daemon-spawned per-event, bounded, exits at turn end — at brain start BEFORE the first reconcile/pulse no current shim is resident, so any `-psyche` psyche-program process alive then is unambiguously stranded-legacy. RESIDUE (doyle PIN 3): the hung wrapper REGISTERED a `-psyche` ready perch before blocking; killing the pid alone leaves a phantom ready-record with a dead pid (the REMOTE-TRUTH presence-lie class) — the sweep MUST also clear that stale registration or prove the existing stale-perch cleanup reaps it. No field window pre-W6 (nothing releases). (F-030 W5) Doc: PAIR 44: REQ-EP-8 <-> REQ-PUMP-PEER-ISOLATION ================================================================================ A) REQ-EP-8 Title: 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. Doc: Status: accepted (2026-06-21) B) REQ-PUMP-PEER-ISOLATION Title: PUMP-TRUTH W2 (architectural, operator ruling 2026-07-08): one peer must NOT block or poison all others -- peer discovery is async / per-peer-independent. Two coupled defects in run_peer_pump: (1) SEQUENTIAL fan-out (for peer in fan_targets dials one-at-a-time, each up to the bound -> peer N+1 waits behind peer N); (2) WHOLE-ROUND POISON (peer_outcome(...)? -- one TimedOut aborts the ENTIRE round via ? -> supervise_pump doubling-backoff restart, resetting ALL conns). FIX: per-peer concurrency + fault isolation -- the pump issues non-blocking dial requests; the broker (already async tokio+iroh) returns connection/presence results as async events (the D4c presence seam), no serial per-peer block; a peer TimedOut drops + reschedules ONLY that peer, NEVER aborts the round or restarts the pump. Supervised-restart is RESERVED for a dead BROKER conn, not a dead peer (the single-thread+bounded-read A-half REQ-HAZARD-PUMP-IPC-DEADLINE was defensive -- it stopped the infinite wedge but coupled every peer's fate; this decouples). Gate: a mixed roster (1 live + N offline peers) -- the live peer connects AND this node advertises presence in the SAME round the offline peers fail; heartbeat advances every round; no PEER_PUMP_RESTART from a dead peer. Depends on W1 (a fast-failing dial is the precondition for clean per-peer scheduling). Doc: PAIR 45: REQ-HAZARD-BIND-REST-STATE-CARRY <-> REQ-SELF-DETECT-PARENT-PID ================================================================================ A) REQ-HAZARD-BIND-REST-STATE-CARRY Title: GATEWAY-LIVENESS DEFECT A (flynn field bug 2026-07-09, confirmed independent): a re-bind MUST preserve the daemon-owned resting intent (rest_state, D9-2/REQ-INST-3) — the same carry-forward discipline establish_perch already applies to cwd/controllable/adapter/read_env. ROOT: establish_perch's record build (crates/spt/src/api/startup.rs, the build closure) constructs a fresh InfoJson via InfoJson::new (defaults rest_state None) and carries cwd/controllable/read_env forward from prior but NOT rest_state -> a re-bind WIPES the wake intent (flynn tick11 rest_state:active vanish). FIX: carry prior.rest_state (and its paired dormant_since_ms anchor, present iff dormant) forward on re-bind, like the sibling fields. Gate: a re-bind over a prior record with rest_state set preserves it (unit — the build closure carries rest_state + dormant_since_ms). KNOWN-HAZARDS entry on landing. Kin REQ-HAZARD-BIND-CWD-UNSET / REQ-PICKER-1 + REQ-INST-3. Doc: B) REQ-SELF-DETECT-PARENT-PID Title: E-1 (REMOTE-TRUTH triage §E-1 #7): self-detect leg (c) — the pid-ancestry fallback — ALSO candidates on `rec.parent_pid` (the harness pid, CONTEXT's 'stable session-binding anchor', stamped at bind), not `rec.pid` alone. ROOT: for an spt-hosted endpoint (broker PTY, headless) `rec.pid` is the ephemeral bind-CLI pid, ALREADY DEAD by send time (the F-026 #11 dead-pid class, field-sighted on hall-bf) — never in any sender's ancestry and alive-gated out — so an spt-hosted sender could NEVER resolve self via leg (c): its messages were from-stamped `cli@NODE` (operator #7) and replies bounced NO_PERCH. FIX: detect_self_by_ancestry pushes a second candidate (id, parent_pid) when `rec.parent_pid` is Some + alive; the pure nearest-first matcher (match_self_by_ancestry) is unchanged. LABEL-ONLY, exactly like the rest of leg (c): from-label/routing default, NEVER authentication — authenticate() untouched, the pid-ancestry-for-auth question stays parked (KH 7.3/7.5 separation holds; a wrong label self-corrects, a wrong grant does not). Env legs (a)/(b) stay first. Red-first int (the triage-specified missing test): rec.pid = dead sibling + rec.parent_pid = genuine live ancestor → self resolves (pre-fix None); ancestry-gate control: live-but-non-ancestor parent_pid must NOT resolve. Rider (same cluster, activated separately once doyle rules the fix shape): F-026 #11 dead-pid itself — rec.pid should hold something that stays true, or liveness readers stop trusting it. Cross-node from-stamp proof (spt-hosted B-side sender arrives at A as `@node`, not `cli@node`) rides the [twohost] rig wave rung. Doc: PAIR 46: REQ-HAZARD-CONFLICT-BOTH-PRESERVED <-> REQ-UPDATE-PROMOTE-DRAINED ================================================================================ A) REQ-HAZARD-CONFLICT-BOTH-PRESERVED Title: 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) Doc: B) REQ-UPDATE-PROMOTE-DRAINED Title: W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED precondition — promote only on ready AND old-gen-subscriber-drained (conn closed OR stall-evicted); the drained signal reads broker truth (the W2 stall-evict tally / the old conn's liveness), no brain round-trip. The residual W2 left open, now closed. Int = a FALSE-PROMOTE rig that exercises the promotion path itself: an old-gen subscriber conn held wedged past ready must NOT promote until it drains (RED-first: ready-alone promotes). Doc: PAIR 47: REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT <-> REQ-INST-4 ================================================================================ A) REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT Title: An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up) Doc: B) REQ-INST-4 Title: active to dormant/suspended fires a transition echo commune Doc: PAIR 48: REQ-HAZARD-ENDPOINT-LIFECYCLE <-> REQ-SHELL-2 ================================================================================ A) REQ-HAZARD-ENDPOINT-LIFECYCLE Title: REGISTRY-LIFECYCLE W2 (KNOWN-HAZARDS 7.45 — the umbrella conformance seam for ADR-0041): endpoint lifecycle state converges to truth from EVERY death path. Regression matrix from the three hertz reports + operator field: dead-PID hybrid row does not survive reconcile; raw viewport close frees the controller (full chain, broker restart included — shared with REQ-STREAM-LEASE-CLASSES int); definitive death means offline+suspended atomically and the next reconcile emits no WAKE_RESUME; explicit Wake still launches exactly once; poll-vs-reap interleave converges to cleared stamps. HEAVY nextest group at birth for any leg spawning a daemon tree. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.45. Doc: 7.45 Endpoint lifecycle state converges to truth from every death path — no optimistic online without authority, no surviving control stamps, no immortal wake intent, no untruthful create `[REQ-HAZARD-ENDPOINT-LIFECYCLE]` Failure (paid-for, three hertz reports + operator field 2026-07-16):** four families, one root shape — lifecycle state written by multiple non-converging paths, optimistic stamps never verified. (a) `cmd_listen` stamped `status=online` from manifest capability alone → dead-PID hybrid rows survived EVERY restart (reconcile skipped them by state, cleanup gated on `controllable= B) REQ-SHELL-2 Title: 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) Doc: PAIR 49: REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK <-> REQ-PICKER-CONTROL-LINE-STATUS-GATE ================================================================================ A) REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK Title: UPDATE-WEDGE round-4 (hertz RCA, root reproduced + source-pinned on Windows 2026-07-09): EVERY write on a physical broker connection rides ONE bounded + cancelable + poison-on-failure framed-write primitive — no writer may hold the connection's serialized send gate across an UNBOUNDED OS write. ROOT: controller_writer held the SharedSend = Arc> guard ACROSS a blocking write_frame; on Windows interprocess 2.4.2 routes the send to WriteFileEx + SleepEx(INFINITE, alertable) with NO supported write timeout (set_timeout → Unsupported), so an `rc --take` controller consumer that stops reading blocks the write INDEFINITELY (~127.95 s in the field capture, released only when a brain restart tore the conns down). Logical stall_evict_controller (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) removes the controller ROLE but neither cancels the in-flight pipe write, closes the physical connection, nor invokes CancelIoEx — the detached writer keeps its SharedSend clone + live stack-owned mutex guard. Load-gated: needs a real seq>0 frame + a non-draining consumer (seq-0 boot conns + a quiescent update are clean — why v0.30.5's controlled apply passed); CTRL_WRITE_LOCKED wait_us=0 on all four capture writers exonerates the mutex convoy — the block is INSIDE the OS write, after lock acquisition. Existing deadlines can't bound it: CONTROLLER_WRITE_DEADLINE is output-driven channel-full handling (a writer blocked on its first owned frame never fills the channel); BRAIN_WRITE_DEADLINE is an age PREDICATE sampled opportunistically, not an I/O timer. FIX (accepted shape, doyle GO + 3 confirms): broker-owned Arc conn object (conn.rs BrokerConn) replacing raw Arc> — serialized write gate (bounded gate-wait) + send half (never leaves the object, so a bypass cannot compile) + idempotent poisoned state + per-in-flight op identity + platform abort seam, with an INDEPENDENT per-conn watchdog firing OUT OF BAND at an ABSOLUTE deadline stamped at write entry covering BOTH gate-wait and OS write completion (brain_write_deadline() — the existing SPT_BRAIN_WRITE_DEADLINE_MS knob; the abort NEVER relies on the write returning or on opportunistic stall-evict sampling). On deadline/partial/cancel/unknown completion: (1) poison the whole physical conn, (2) abort read+write (cfg(windows) CancelIoEx then DisconnectNamedPipe; cfg(unix) UnixStream::shutdown(Both)) so handle_conn reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer (interprocess write_exsync returns only after the completion APC — release = write return), (4) NEVER reuse the conn (a timed-out length-prefixed frame may be partially written), (5) join/finish the retired writer before reporting physical cleanup. Controller (replay + live), viewer, dispatch-reply (send_frame/send_error + inline dispatch_* replies), and nethost stream-log/presence writes ALL route through the primitive — leaving ANY raw unbounded write behind the gate preserves the failure class (grep-proven: no surviving raw send.lock()+write_frame on a physical conn path). NO new output queue (the bounded queue + isolated writer exist; the block is BELOW them); NO PIPE_NOWAIT (recorded mid-frame corruption risk). Broker-side only, no wire change, mixed-version peers wire-compatible; Unix keeps existing semantics under the same poison/retire invariant. Doc: 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]` Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_t B) REQ-PICKER-CONTROL-LINE-STATUS-GATE Title: A2 (F028, operator #2): the picker confirm-panel 'controlled locally' line renders for OFFLINE endpoints. view.rs:425-436 builds control_line from ep.controlled with NO status gate; an offline endpoint with a stale controlled stamp shows 'controlled locally' (operator screenshot: hall-a offline + controlled locally). RENDER HALF (this REQ): control_line MUST be empty when status != Online. The upstream STICKY-stamp half (stamp survives client SIGKILL >=5min) is B3/REQ-PRESENCE-CONTROL-REAP-ON-EXIT. FIX: gate the render. See triage A2(a). Doc: PAIR 50: REQ-INST-3 <-> REQ-MESH-6 ================================================================================ A) REQ-INST-3 Title: Dormant (warm) / suspended (cold) resting states Doc: Dormancy resource budget (D9-3 — ADR-0003 red-team #9) B) REQ-MESH-6 Title: 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.) Doc: PAIR 51: REQ-LIST-JSON-LIVENESS-PARITY <-> REQ-PRESENCE-CONTROL-REAP-ON-EXIT ================================================================================ A) REQ-LIST-JSON-LIVENESS-PARITY Title: GATEWAY-LIVENESS (flynn field bug 2026-07-09, RCA reader-divergence root): `spt endpoint list` (human) and `endpoint list --json` MUST report an IDENTICAL status for a locally-hosted endpoint — especially a pid-alive, status-ABSENT gateway (no psyche_init). ROOT (todlando RCA STEP-1, doyle-verified): the --json builder (crates/spt/src/cli.rs cmd_endpoint_list) emits each subnet row's status straight from resource_projection (spt-net registry.rs:566, passes instance.status through verbatim :592 — the persisted WAN snapshot, a lagged gossip that can carry a stale/crash-time Suspended) and NEVER applies the self-owned reconcile the human/picker path applies (reconcile_self_owned, crates/spt/src/picker/data.rs:160 via gather_endpoints :112). So a pid-alive self-owned gateway reads Suspended on --json but ONLINE on human (roster::enumerate spt/src/roster.rs:38 -> is_perch_alive pid-fallback spt-store/liveness.rs:136); the adapter suspend-poll (parse_endpoint_status over endpoint list --json --show-all) reads the divergent --json status -> self-suspends a pid-alive gateway. Candidates REFUTED: resource_projection does NOT re-derive liveness (copies instance.status, only skips !routable :582); render is a faithful {:?}. advertised_status (registryhost.rs:822) DOES compute Active via pid-fallback (live advertise fine) — the bug is the READER showing the un-reconciled snapshot. FIX (doyle-ruled): apply the self-owned reconcile in the cli --json builder (SAME spt/picker consumer layer as gather_endpoints, NOT lifted into resource_projection which inverts the dep spt-net->perch), reading LOCAL perch truth (is_perch_alive/unbound -> Active/Offline, mirroring the picker's local_rows). Do NOT force status=online (DEFECT B latent — seed-#5 orphan-listener false-ONLINE risk). Gate: a pid-alive locally-hosted gateway (status-absent, no psyche_init) reads the SAME non-Suspended status on human AND --json. Field-verify flynn (mobile-gw). Kin REQ-PICKER-3 + REQ-PRESENCE-LIVENESS-TRUTH + seed-#5 orphan-listener false-ONLINE. Doc: B) REQ-PRESENCE-CONTROL-REAP-ON-EXIT Title: B3 (F028, hall-b diagnosis + deferred #11 seed; BROADENED perri F-b CONFIRMED): dead-pid ONLINE decay window + sticky CONTROLLED stamp. Repro: /exit -> all endpoint processes dead -> `endpoint list` stays ■ ONLINE for a decay window before OFFLINE. Sticky CONTROLLED: perri confirmed controlled=true + attached_node SET while alive=false/OFFLINE, persisting >20min AND ACROSS A DAEMON RESTART (hall-b) — worse than the SIGKILL>=5min original (CAVEAT still: may reflect claude's --remote-control channel not the PTY attach — DISAMBIGUATE first). This is the deferred #11; RCA belongs to this wave. FIX: reap must clear presence AND control stamps promptly across FOUR paths — (i) clean exit, (ii) serve conn-drop, (iii) session-died-without-exit (crash/bounce), (iv) a BOOT-TIME sweep so a restarted daemon does NOT resurrect control stamps for endpoints it can see are dead. Int tests per edge. Closes A2(b). See triage B3 (broadened). Doc: PAIR 52: REQ-MSG-INJECT-LEG-DROP-VISIBLE <-> REQ-UPDATE-PROMOTE-DRAINED ================================================================================ A) REQ-MSG-INJECT-LEG-DROP-VISIBLE Title: SEED (inactive — observability): a silently-dropped delivery leg must be DISTINGUISHABLE from an honestly-offline endpoint on a status surface. The spt-hosted inject leg (spt-daemon inject.rs `try_spt_hosted_inject` — the ONE shared implementation behind local cmd_send, the WAN ingress, the idle-edge drain, and the parked-idle/pulse re-offer belts) gates on `is_spt_hosted_no_relay` → `deliver::is_online` → `liveness::is_perch_alive`: a perch PINNED TO A DEAD SESSION (the KH 7.25 wedge class — dead owner, record not yet healed by the next auth touch) makes EVERY belt on that leg silently return None → messages spool as if the endpoint were ordinarily offline, while the operator-facing view can keep reading the recorded state. Nothing anywhere surfaces 'the inject leg stopped firing for this endpoint' — the idle-window injections just stop, which from the outside is indistinguishable from 'no messages arrived' (field shape: IDLE-EDGE W1 field-verify, perri's killed-resident rig — their 'so it did not repeat' conclusion was exactly this invisibility, self-corrected only by re-rigging with a live resident). Shape at activation: a status/list surface DERIVES and reports the inject-leg verdict per endpoint (inject-target / not-a-target + WHY: no perch, dead session pin, no relay, not controllable) so a dropped leg is a READABLE state, never an inferred one; additive-evolution posture on any --json surface. Kin: REQ-SHELL-LIST-DERIVED-PROVENANCE (view-vs-truth, same class), KH 7.25 (`REQ-HAZARD-SESSION-PIN-WEDGE`, the pin that drops the leg), KH 2.5 (liveness authority lives in ONE resolver — the verdict here REPORTS through it, never re-derives beside it). Doc: B) REQ-UPDATE-PROMOTE-DRAINED Title: W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED precondition — promote only on ready AND old-gen-subscriber-drained (conn closed OR stall-evicted); the drained signal reads broker truth (the W2 stall-evict tally / the old conn's liveness), no brain round-trip. The residual W2 left open, now closed. Int = a FALSE-PROMOTE rig that exercises the promotion path itself: an old-gen subscriber conn held wedged past ready must NOT promote until it drains (RED-first: ready-alone promotes). Doc: PAIR 53: REQ-PID-ROLE-EVIDENCE <-> REQ-UPDATE-TRIAL-DRAIN-DRIVE ================================================================================ A) REQ-PID-ROLE-EVIDENCE Title: THE RECORDED PID MUST CARRY WHAT IT MEANS. `info.pid` has two incompatible meanings decided by write path, and every reader has been guessing: the `api listen` path records a process that GENUINELY HOLDS the endpoint (its death IS the endpoint's relay death), while the `api bind` path records the announcing CLI, which is EXPECTED TO EXIT within seconds and whose death means NOTHING — the hosting life it announced (a broker PTY session) is not recorded anywhere in the row. LIVE MEASUREMENT (todlando, HFENDULEAM 2026-07-27, the finding that forced this mint): todlando pid 22588 DEAD, doyle pid 45160 DEAD, deployah pid 29176 DEAD — all controllable=true, state=live_agent, status=online, and all three GENUINELY ALIVE AND WORKING (the measurement was taken by one of them, messaging another) — against hertz pid 11216 ALIVE and mobile-gw pid 46152 ALIVE, which are real `api listen` relays. The split is by adapter integration pattern (claude-spt binds and exits; omp-spt/mobile hold a listener), NOT by anything a reader can see in the record. CONSEQUENCE ALREADY PAID: two proposed fixes for the emphasys convergence gap were BOTH falsified pre-build on this fact — oracle-first convergence, and carry-forward scoped to 'earning pid alive' — each would have converged three live agents node-wide. Both were keyed on a pid whose meaning they could not read. FIX: stamp the role at the seam that writes the pid — `relay` on the listen path, `binder` on every bind path — never inferred at read time. Absent ⇒ legacy row ⇒ NO KNOWLEDGE, and every consumer fails toward alive (inheritance stands, convergence never fires); such rows heal at their next re-bind. Consumers re-key on it: relay-death convergence fires only on `relay`, and the controllable carry-forward drops a stamp only when a prior `relay` pid is provably Gone. CLASS: a claim keyed on the wrong thing (kin: `is_perch_alive` reading a status FIELD as hosting topology; the inherited capability stamp routing a liveness proof) — the cure is to make the record SAY the thing rather than have readers infer it. AUDIT RIDER (doyle, required in the doc stage since convergence re-keys on this field): enumerate which row classes reach the convergence branch under the new key — fresh NonAgent/None rows, shell instances, gateway listens — and pin the answer structurally, not just for tonight's node. Gate: doc — the record-shape doc carries the field, the two meanings it ends, the legacy/no-knowledge rule, and the audit-rider enumeration; impl — the field on InfoJson stamped from the entry path at the bind seam, plus both consumer re-keys; unit — the role-stamp table over the three hosting authorities, the carry-forward table incl. the claude-spt-shaped NEGATIVE (binder-role dead pid + Some(true) + listener re-bind ⇒ inheritance STANDS, row never routes to convergence), and the convergence role gate incl. the legacy-absent row; int — the synthetic emphasys template (relay-role + dead pid/parent + valid birth stamp + listener-only re-bind ⇒ derives fresh ⇒ routes to convergence ⇒ oracle Gone ⇒ converged) with a BrokerPty sibling that re-asserts fresh and is untouched. Doc: `info.json` — what the recorded `pid` MEANS (`pid_role`) B) REQ-UPDATE-TRIAL-DRAIN-DRIVE Title: UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze all 7 live PTYs ~30s then rolled back): the promote gate (run_trial, brainproc.rs:657-661) needs BOTH `ready_generation==gen` AND `old_gen_drained()`; `old_gen_drained()` = `!any_local_controller_wedged()` (brainproc.rs:534) is a PURE READ of `write_blocked_since` (broker.rs:2703) — it never DRIVES the evict. The evict (`stall_evict_controller`, broker.rs:1039, same 15s `brain_write_deadline` the wedge-read uses) only runs via `reap_dead_controller` (broker.rs:967, severed->drop ELSE stall-evict) inside the KIND_SESSIONS snapshot closure (broker.rs:2879). During the isolated brain-trial window NOTHING polls KIND_SESSIONS: the old brain was hard-killed (`child.kill()`, brainproc.rs:851) so its local controller conn black-holes (live PTYs keep writing to a dead Windows named pipe -> writer BLOCKS, never EOFs) -> `write_blocked_since=Some` -> wedged=true for the full 30s -> gate false -> `WindowElapsedAlive` -> kill+rollback. The candidate DID reach ready (write_ready, brainproc.rs:211, runs before the loop; the v53 log's NET_FAMILY_GATE/PAIR_MEET_UP prove the loop was entered) — so `BRAIN_TRIAL_TIMEOUT: candidate alive but never ready` is the MISLEADING ready-stamped-but-never-DRAINED case, NOT a resume_sessions hang. SECOND LEG (recovery): post-rollback the gen-2 brain's KIND_SESSIONS poll finally reaps -> `BRAIN_SUBSCRIBER_STALL_EVICT:1` fires >15s late -> a session stayed black-holed through recovery -> continued freeze. ONE root, BOTH legs. FIX (BRAIN-SIDE, self-applying — doyle ruled brain-side to AVOID a broker-side coordinated-restart flag): the boot/trial heartbeat loop (brainproc.rs run_brain, currently only `net_status` at :244) also issues `Brain::sessions()` (KIND_SESSIONS, brain.rs:1397 — already exists) every heartbeat, driving the LIVE older broker's ALREADY-SHIPPED reap (>=v0.29.0 LIFECYCLE-TRUTH; the field-stuck broker is v0.30.0 so it HAS it). The old-gen wedged conn is stall-evicted ~15s < the 30s window -> `old_gen_drained()` flips true -> PROMOTE; the same poll on the rollback/recovery brain reaps promptly -> no >15s black-hole -> kills the STALL_EVICT recovery leg. Drive it on the FIRST heartbeat (no one-tick wait) and on BOTH the trial candidate AND the recovery brain (one loop covers both). SELF-APPLIES because it drives the current broker's existing reap verb — v54's brain fixes the v0.30.0->v54 update with NO coordinated broker restart. Forward-compat: brokers rollback); (2) a deliberately-failing trial auto-rolls-back AND the restored brain re-drives EVERY session with NO BRAIN_SUBSCRIBER_STALL_EVICT. Doc: brain-trial promotion (readiness + drained)** — the broker supervises the swapped-in brain through a bounded readiness **trial** and *promotes* the new binary only when it both signals ready for its own generation **and** the OUTGOING generation's control plane has **drained** — the old brain's local (brain-owned) controller connection is closed or stall-evicted, never still holding blocked writes. A hard-killed prior generation leaves that connection **black-holed** (its hosted PTYs keep producing output the broker's writer blocks on, since a killed peer's pipe blocks rather than EOFs), and t PAIR 54: REQ-RELAY-DEATH-CONVERGENCE <-> REQ-WAKE-RESUME-LEG ================================================================================ A) REQ-RELAY-DEATH-CONVERGENCE Title: A dead harness relay MUST converge the endpoint's projection — an endpoint whose relay process is provably gone cannot keep rendering `ONLINE - HARNESS ONLY` with ready=true alive=true and a registered address merely because the owner parent is still alive. RE-PROVEN PRESENT on 0.44.0 (hertz W0 item 2, 2026-07-27, fresh isolated build sha-prefix 00e15f): relay pid 38764 killed 06:21:01Z, owner pid 12156 live; 148s later info.json still pid=38764 status=online, list JSON ready=true alive=true address=127.0.0.1:57709, TCP to that address unreachable, human list `ONLINE - HARNESS ONLY`. Sibling probes: live-relay sibling's socket reachable and `send` -> SENT with the EVENT received (the rig CAN see delivery); clean `endpoint stop` sibling rendered status=offline ready=false alive=false (the rig CAN see convergence). FIX SHAPE: relay-death convergence — a dead/unreachable relay clears the ready/address/liveness projection for its endpoint within a bounded window, in ONE authority: the verdict MUST route through the shipped custody/process-identity predicate (KH 2.5 — liveness authority lives in one resolver, never re-derived beside it; Unproven never kills). Kin: the Athenaeum stale-ALIVE generator (HOSTING_AUTHORITY_DEMOTED demote-to-no-claim arm, REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY — the demote-vs-offline fork needs the same identity predicate), SHELL-STALE-ONLINE (same lying-signal class, shell flavor), REQ-MSG-INJECT-LEG-DROP-VISIBLE (the observability face of the same wedge). TITLE ADDENDUM 2026-07-27 (doyle ruling, todlando build): THE VERDICT FIRES ONLY WHERE THE RECORDED PID WAS A **HOLDER** (pid_role=relay, REQ-PID-ROLE-EVIDENCE). The shipped branch asks the oracle about whatever pid the record carries, and `info.pid` means two different things by write path — a holding `api listen` relay, or an `api bind` announcer that exits within seconds. Measured on HFENDULEAM 2026-07-27: todlando 22588, doyle 45160, deployah 29176 all DEAD-and-online (binder pids) against hertz 11216 / mobile-gw 46152 alive (relay pids), so an unrole-gated verdict would have offlined three live agents. A row with no role stamp (legacy) is NO KNOWLEDGE and never converges; it heals at its next re-bind. Doc: B) REQ-WAKE-RESUME-LEG Title: A-2 (REMOTE-TRUTH triage §A-2 + ADR-0033): the daemon reconcile gains a WAKE-RESUME LEG — an endpoint whose rest INTENT is Active but whose harness session is COLD (status != online) is resumed by the daemon via the adapter's [session.resume] template using the LAST LEDGER session id, so a bare `spt wake ` on a suspended live agent actually brings it back (today: reconcile_once start-arm hosts ONLY status==online (livehost.rs:199), so a woken-but-unbound endpoint is skipped forever — neither status reaches online nor does reconcile re-host). This is the ADR-0033 LIFT: the thin `spt wake` edge writes rest intent, the DAEMON does the work. Mirrors shellwake::resolve_wake (read rest state, live-pid double-launch guard, launch, NEVER flip status — the harness self-binds → online). The leg reads the recorded adapter (D-2, REQ-SESSION-ADAPTER-RECORDED); an UNREGISTERED recorded adapter is the Q5 daemon-variant refuse: do NOT spawn, record a LOUD host_error report (F-1 naming the adapter + `spt adapter add`), never silent, never fallback-spawn on a different adapter. BINDS: (1) status=online is set ONLY by a real bind — the resume leg NEVER stamps it (CONTEXT liveness truth; the A-1 effective-state derivation depends on this staying honest). (2) host_error is a REPORT of the most recent host-level failure, NEVER a liveness input — neither liveness nor advertised_status reads it (host_error + online still derives Active); cleared on a successful host/bind; the existing silent `continue` on a deregistered online adapter (livehost.rs:205) folds into the same field. (3) the resume-pid guard marker is CUSTODY-ONLY (F-030 nested-record discipline) — never a liveness input. cold-with-no-ledger-row degrades benign (loud-logged skip, no crash, today's behavior). Single-node; C-2 picker Wake-now unblocks after. --wait is a SEPARATE rider (REQ-WAKE-WAIT). Doc: PAIR 55: REQ-SPAWN-COLLISION-GUARD-LIVE-DUP <-> REQ-UPDATE-PROMOTE-DRAINED ================================================================================ A) REQ-SPAWN-COLLISION-GUARD-LIVE-DUP Title: W4 (LIFECYCLE-TRUTH): single-flight wake per endpoint — the WAKE/RESUME respawn seam must not launch twice for one wake. ROOT (perri parentage + recovered filing): one wake processed TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume ` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. DAMAGE: duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a fresh /clear rotation -> injects routed to the contended record and lost). FIX: single-flight wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim. Int: two concurrent wake requests -> exactly one launch tree. Doc: B) REQ-UPDATE-PROMOTE-DRAINED Title: W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED precondition — promote only on ready AND old-gen-subscriber-drained (conn closed OR stall-evicted); the drained signal reads broker truth (the W2 stall-evict tally / the old conn's liveness), no brain round-trip. The residual W2 left open, now closed. Int = a FALSE-PROMOTE rig that exercises the promotion path itself: an old-gen subscriber conn held wedged past ready must NOT promote until it drains (RED-first: ready-alone promotes). Doc: PAIR 56: REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY <-> REQ-TERM-6 ================================================================================ A) REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY Title: An online agent-family endpoint with NO session surface reads as message-reachable, not as a harness-hosted live agent and not as a plain ONLINE. (hertz v0.39.0 field report 2026-07-21; doyle PRODUCT RULING — deliberately NEITHER of the two options offered.) OBSERVED: an adapterless `spt ready` perch (type=ready_agent, adapter=null, ready/alive true) displays plain ONLINE with '(unknown adapter)'. SOURCE: picker/model.rs display_status returns Online for every non-live_agent type (~680-681) BEFORE consulting controllable, and amber HarnessOnly is live-agent-only (~683-687). RULING: that type gate is CORRECT and STAYS. 'ONLINE - HARNESS ONLY' means one specific thing — a LIVE AGENT whose session surface is owned by a harness rather than a broker PTY. Broadening it to 'online non-controllable agent-family' would make one label mean two different things, which is how a status label starts lying. An adapterless ready receiver is a THIRD truth: message-reachable, no session surface at all, nothing to attach to ever. So: a DISTINCT display state (working name 'ONLINE - MESSAGE ONLY') keyed on the endpoint TYPE (ready_agent), never on absence-of-adapter, and never an invented adapter name. SECOND RULING (same surface, separate lie): '(unknown adapter)' is itself a small diagnostic untruth — adapter=null is ABSENT, deliberately so, not unknown; the copy must say absent. Gate: impl — the distinct display state + the absent-adapter copy; unit — the display table gains the ready_agent row and the existing live_agent/gateway rows are UNCHANGED (this must not perturb the HarnessOnly gate), plus a label assertion for the new state. Doc: B) REQ-TERM-6 Title: 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. Doc: PAIR 57: REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT <-> REQ-SPAWN-COLLISION-GUARD-LIVE-DUP ================================================================================ A) REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT Title: 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) Doc: B) REQ-SPAWN-COLLISION-GUARD-LIVE-DUP Title: W4 (LIFECYCLE-TRUTH): single-flight wake per endpoint — the WAKE/RESUME respawn seam must not launch twice for one wake. ROOT (perri parentage + recovered filing): one wake processed TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume ` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. DAMAGE: duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a fresh /clear rotation -> injects routed to the contended record and lost). FIX: single-flight wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim. Int: two concurrent wake requests -> exactly one launch tree. Doc: PAIR 58: REQ-DSR-SINGLE-CPR <-> REQ-SHELL-2 ================================================================================ A) REQ-DSR-SINGLE-CPR Title: TEARDOWN-AUTHORITY W3 rider (hertz RCA 2026-07-19): a Device Status Report query yields EXACTLY ONE Cursor Position Report. Rides W3 because it is the same emulator-conformance surface as the width work and the same field capture surfaced it; kept a separate REQ so its evidence is not buried inside the width tags. Gate: impl — one CPR per DSR on the emulator reply path; unit — a DSR in the input stream produces a single well-formed CPR carrying the DISPLAY-column cursor position (i.e. consistent with REQ-SCREENGRID-WIDTH reckoning, not the raw char count). Doc: B) REQ-SHELL-2 Title: 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) Doc: PAIR 59: REQ-HAZARD-CASCADE-WIPE-GUARD <-> REQ-SHELL-2 ================================================================================ A) REQ-HAZARD-CASCADE-WIPE-GUARD Title: No hard-delete of a parent hosting non-empty children (6.3) Doc: B) REQ-SHELL-2 Title: 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) Doc: PAIR 60: REQ-ACL-ACCESS-REFRESH-ER-ONLY <-> REQ-ER-BRINGUP-PROMPT ================================================================================ A) REQ-ACL-ACCESS-REFRESH-ER-ONLY Title: spt api access-refresh stops refusing and becomes real, gated to the engine room, updating ONLY the node's captured subnet-level fallbacks (ADR-0052 decision 6; the W2 refusal REQ-ACL-ACCESS-REFRESH-VERB was minted precisely so this wave changes one behavior rather than adding a surface and its gate). The refresh is the CONSENTED half of advisory gossip: a subnet-mode change reaches a member as a notification, the engine room is briefed with the exact new posture, and a human decides whether this node adopts it — which is why the verb writes the captured fallbacks and never the node's own rules, and why no remote actor can invoke it. Its authentication is the same shared engine-room function empower uses. The W2 refusal test is repinned to the new contract in this same change, not left asserting a behavior the wave removed. Gate: doc — ADR-0052 decision 6 and the CONTEXT.md capture-refresh sentence; impl — the real refresh behind the shared engine-room gate, writing captured subnet fallbacks only; unit — an engine-room caller refreshes and the chain's bottom tier changes, a non-engine-room caller is refused, the node's own rules are untouched, and the repinned W2 test asserts the new contract. Doc: A node's **effective** subnet modes are captured at its join and are immutable from outside. Mode changes replicate as advisory metadata: they surface as a notif, and the engine-room is briefed at session start with the exact new posture, encouraged to offer the user a sync. The capture-refresh is an `spt api` verb **only the engine-room can invoke**, and it updates only the captured subnet-level fallbacks — never the node's own rules. No remote actor can change a node's effective posture. B) REQ-ER-BRINGUP-PROMPT Title: Bare `spt rc engine-room` prompts interactively for the bring-up TOTP instead of attaching straight into the gate's refusal (ADR-0052 §2 entry UX, fast-follow grill 2026-07-30). The prompt is the preferred entry because argv is readable by other processes on the box while a code is still valid; `--code ` stays for scripted/non-interactive use with that caveat documented in its help. Prompt rules: Esc-cancel (the ceremony-overlay pattern) and ctrl-c both cancel with NO attach attempt even when the buffer holds digits; Enter submits a non-empty buffer and is inert on an empty one; only ASCII digits accumulate, length-bounded, so pasted garbage never rides into the gate and spends ledger budget; every other endpoint never sees a prompt; and a non-terminal stdin refuses with guidance naming --code rather than blocking on a pipe. Gate: doc — the ADR-0052 §2 entry-UX note; impl — the prompt behind the bare invocation, the pure key-fold kernel, and the non-terminal refusal; unit — the key-fold rules (digits-only accumulation, bound, backspace, inert-empty-Enter, submit, and both cancel paths beating a full buffer). Doc: Entry UX (ruled 2026-07-30, fast-follow grill):** bare `spt rc engine-room` opens an interactive TOTP prompt (Esc-cancel overlay, the releases#19 pattern) `--code ` stays for direct passing, with the documented caveat that argv is readable by other processes on the box while the code is still valid. PAIR 61: REQ-ER-BRINGUP-PROMPT <-> REQ-HAZARD-INJECT-WORKER-POISON ================================================================================ A) REQ-ER-BRINGUP-PROMPT Title: Bare `spt rc engine-room` prompts interactively for the bring-up TOTP instead of attaching straight into the gate's refusal (ADR-0052 §2 entry UX, fast-follow grill 2026-07-30). The prompt is the preferred entry because argv is readable by other processes on the box while a code is still valid; `--code ` stays for scripted/non-interactive use with that caveat documented in its help. Prompt rules: Esc-cancel (the ceremony-overlay pattern) and ctrl-c both cancel with NO attach attempt even when the buffer holds digits; Enter submits a non-empty buffer and is inert on an empty one; only ASCII digits accumulate, length-bounded, so pasted garbage never rides into the gate and spends ledger budget; every other endpoint never sees a prompt; and a non-terminal stdin refuses with guidance naming --code rather than blocking on a pipe. Gate: doc — the ADR-0052 §2 entry-UX note; impl — the prompt behind the bare invocation, the pure key-fold kernel, and the non-terminal refusal; unit — the key-fold rules (digits-only accumulation, bound, backspace, inert-empty-Enter, submit, and both cancel paths beating a full buffer). Doc: Entry UX (ruled 2026-07-30, fast-follow grill):** bare `spt rc engine-room` opens an interactive TOTP prompt (Esc-cancel overlay, the releases#19 pattern) `--code ` stays for direct passing, with the documented caveat that argv is readable by other processes on the box while the code is still valid. B) REQ-HAZARD-INJECT-WORKER-POISON Title: The per-session inject-worker floor Mutex is SHARED by the inject worker (open/flush) and the controller-input path (dispatch_input Layer C buffer_if_held); a panic under the lock on EITHER poisons it, and a bare .lock().unwrap() at the next site then panics too — a panic in the inject WORKER kills its thread WITHOUT reaping the translation child, orphaning a live binary while event_tx.send fails, so force-native reports 'worker-gone' delivered=false FOREVER (the F-e generic-miss shape). HARDENING, NOT the F-e incident root (perri's matrix exonerated poison under thrash/dormancy/churn): (i) poison-tolerant floor lock (unwrap_or_else into_inner) at all 3 sites so one panic can't cascade the session's delivery dead; (ii) a panic-resilient inject worker (catch_unwind -> fault+terminate the child on a worker panic) so a dead worker never orphans a live binary + strands delivery. Poison-tolerance class of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE / bug #16. Doc: PAIR 62: REQ-ER-SESSION-BRIEFING <-> REQ-PICKER-PURGE-SHORTCUT ================================================================================ A) REQ-ER-SESSION-BRIEFING Title: Every engine-room session opens with a system-authored briefing message stating its capabilities, its responsibilities, the node's exact current access posture, and any pending advisory-gossip deltas (ADR-0052 decision 3 and decision 6; doyle ruling (e) 2026-07-29). A minded governance surface is only as good as what it knows at the moment it acts, and posture is per-session state — which is why the briefing is a message spooled at bring-up under a reserved system author, NOT the endpoint's durable role text: role is durable identity, mutating it per session would race the role editor and pollute the mind's substrate. The message spool is already core-side, harness-independent data that any adapter renders as ordinary inbound, so the briefing needs no harness-specific cooperation — the boundary rule holds by construction. Gate: doc — ADR-0052 decision 3's briefing clause and the CONTEXT.md engine-room entry; impl — the briefing composer and its spooling at bring-up under a reserved system author; unit — bring-up spools exactly one briefing carrying capabilities, responsibilities, current posture and pending gossip deltas, and no role text is written. Doc: Refuses all inbound except replies to its own outbound (knocks and knock-codes ARE accepted); online **only while a controller is attached** — detach drops it offline and every empowerment dies with it; `rc --view` denied even locally; remote attach denied; **local `rc --take` allowed** precisely because it forces a harness restart and revokes all empowerments; not registry-advertised by default (only to endpoints it has whitelisted); every session start delivers a briefing message stating its capabilities and responsibilities; it presents access rulesets as tables. B) REQ-PICKER-PURGE-SHORTCUT Title: C-3 (REMOTE-TRUTH triage §C-3 #8): the pick-existing list gains an `x` purge shortcut — on an OFFLINE LOCAL highlight, `x` opens a small in-TUI confirm screen (Screen::ConfirmPurge, the B-2 ChangeAdapterPick shape) and Enter purges via the ONE existing purge core `cmd_endpoint_purge(id, yes=true, force=false)` — NEVER the core's stdin [y/N] (it fights the picker's raw mode; the confirm screen IS the confirm). The shortcut INHERITS both purge gates (offline-only + node-local, cli.rs cmd_endpoint_purge / CONTEXT:189): gated-off presses stay on the list and FLASH WHY (online → offline-only, remote → local-only). force=false is deliberate — the model gate is advisory; the core's own offline check is the authority, and a race to online between gate and purge must REFUSE, never stop-then-purge. After a successful purge the picker STAYS (inline outcome, like Shortcut/ChangeAdapter): the row leaves the in-memory list (remove_endpoint, cursor re-clamped) + flash PURGED:{id}. Hint truth (B-1/F029 discipline): the pick legend renders `x purge` ONLY when purge_key_live() — the same predicate the handler gates on. Red-first: purge outcome reachable ONLY from an offline LOCAL highlight (online/remote → no screen change + why-flash). Doc: PAIR 63: REQ-IDLE-PARKED-DELIVERY <-> REQ-LIVE-AGENT-NO-INJECT-DELIVERY ================================================================================ A) REQ-IDLE-PARKED-DELIVERY Title: W5 (LIFECYCLE-TRUTH): a message QUEUED to an ALREADY-idle spt-hosted endpoint is delivered without an operator poke. ROOT (live during the milestone dispatch 2026-07-07): the idle-edge drain (F-023 leg 2) fires only on the ACTIVE->IDLE transition; no new edge ever comes for a parked session, and the send-time inject didn't carry it — both doyle->todlando dispatches sat delivered=0 in the spool while the endpoint showed ONLINE. FIX: send-time inject fires for an already-idle spt-hosted target (activity sense says idle => inject now, not spool), and/or a bounded spool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop). Int: send to a session idle for N minutes -> delivered without any operator poke. Doc: B) REQ-LIVE-AGENT-NO-INJECT-DELIVERY Title: F-033 RE-SCOPED (doyle re-ruling 2026-07-10 after the #82 gate falsified the original premise — the hosting-mode class split is BINDING context): 'state:live_agent has a self-delivery reader' CONFLATED two hosting modes. (A) HARNESS-hosted live agents (api listen path) DO deliver via adapter channels only — and are ALREADY structurally excluded from inject: bind_from_seed stamps controllable=Some(false), no broker PTY exists. (B) SPT-HOSTED/CONTROLLED live agents' inject leg IS their delivery reader (broker PTY + translation binary — doyle's own endpoint is field proof: ENDPOINT_INJECT + IDLE_PARKED_DRAIN alongside hook-poll); the original blanket state:live_agent exclusion broke REQ-MSG-IDLE-EDGE-DRAIN + the v0.14.3 LAW on both CI platforms and was REVERTED (predicate stays controllable-gated). perri's adapter-channels-only model (F-dupmsg-adapter-confirm.md) holds for class (A) only — do not re-seed the conflation. The F-033 DUPLICATE mechanism (hook-poll + idle-inject both delivering one row, operator spool row 156) is closed structurally by REQ-CARRIER-CLAIM-EXCLUSIVE's atomic cross-carrier take. REMAINING LEGS OF THIS REQ: (a) unit — a harness-hosted live agent (controllable Some(false)/None, no broker PTY) can never route through try_spt_hosted_inject (evidence may TAG the existing is_spt_hosted_no_relay non-controllable case rather than duplicate it); (b) VERIFICATION (report-before-fix, DELIVERED to doyle 2026-07-10): the no-translation-binary raw payload+CR path is PROVABLY DEAD (broker dispatch_endpoint_input: no-binary -> loud spool, never a PTY write — v0.14.3 holds); the operator's typed-unsubmitted garbage is PINNED to the Layer-2 echo-verify RE-DRIVE (broker.rs inject worker) force-enabled host-wide by ambient SPT_INJECT_VERIFY_ECHO in the daemon's inherited dev-shell env (default-OFF declared capability turned on globally — the F-036 env-inheritance class): a false verify-miss RETYPES the whole sequence into the input field. Fix LANDED on the W4 branch (doyle-accepted echo-scrub 2026-07-10): SPT_INJECT_VERIFY_ECHO/SPT_INJECT_FORCE_ECHO_MISS folded into the W1 daemon-startup env scrub (spt_runtime::INJECT_ECHO_ENV_VARS; startup-only, role-spawn builder untouched so explicit per-spawn declaration stays the production on-switch) — evidence rides REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE (the F-036 class REQ). Residual-class RULED (doyle 2026-07-10): this REQ covers the harness-hosted-never-inject predicate leg (unit) + the echo-scrub itself (impl, dual-tagged) — stages [impl,unit]. Doc: PAIR 64: REQ-ADAPTER-FLOOR-ENFORCE <-> REQ-HAZARD-DEFERRED-MANIFEST ================================================================================ A) REQ-ADAPTER-FLOOR-ENFORCE Title: F-5 (REMOTE-TRUTH triage §F-5 + doyle rulings 2026-07-05): BOTH adapter acquisition verbs (spt adapter add + spt adapter update) REFUSE when the installed spt-core is BELOW the adapter's declared [adapter].min_spt_core_version floor — with an F-1 operator refusal naming the installed core, the floor, and the next action (update spt-core first). ROOT: the floor was PARSED + required (manifest.rs) but never compared to the running core — dead enforcement; and the [update].version_check knob that gated it was DOC'D-BUT-DEAD (never read by any production path — a contract lie). RULINGS: RETIRE version_check (drop the manifest field + schema + docs + the cfg(test) literals; a pre-existing manifest still setting it deserializes fine — serde ignores the unknown key, no deny_unknown_fields, so retiring is back-compatible); SEMVER-compare NOT string-compare (the 0.9.0 < 0.25.0 lexical trap); enforce on BOTH verbs; nothing installs / registry untouched on refuse (binds both verbs, no residuals). FIX: (1) a pure spt-runtime version_meets_floor(core, floor) -> bool (numeric per-component: split '.', u64, missing→0, non-numeric→0, first-diff decides, equal-when-zero-padded ⇒ satisfied) — mirrors the CLI version_is_newer parse (same numeric model, different question: freshness=strictly-newer vs floor=at-least). (2) ADD: the gate lives INSIDE registry::register (the choke point) via a register_with_core(core_version) seam register() delegates to with env!(CARGO_PKG_VERSION) — the floor check runs right after the manifest parse, BEFORE any registry write, returning the typed RegistryError::CoreFloor{adapter,core,floor} (Display = the ONE F-1 refusal both verbs surface); nothing recorded on refuse. (3) UPDATE: a PRE-SWAP peek (staged_floor_ok) extracts the staged .spt to a THROWAWAY temp, parses its manifest floor, and refuses BEFORE apply_release_crc_swap mutates the live pointer-mode home — so a refusal (or an unverifiable floor: FAIL-CLOSED) leaves the live install BYTE-UNTOUCHED; register@8932 stays as the defense-in-depth backstop for every other entry path. doyle bind: the register-only gate would let the crc-swap replace the live files with a floor-violating version while the record refuses (record and reality disagree — the exact contract-lie shape this milestone kills), so the pre-swap peek is the only correct answer. Red-first: perri negative repro on ADD (fresh home + synthetic low core + high-floor manifest → CoreFloor refuse, registry untouched) + the UPDATE pre-swap refuse (live home byte-untouched) + a floor-met positive control (0.25.0-on-0.25.0 installs); + version_meets_floor table incl. the 0.9<0.25 trap. Doc: `min_spt_core_version` is the **enforced** compatibility floor. Both acquisition verbs — `spt adapter add` and `spt adapter update` — REFUSE when the installed spt-core is below this version, naming the installed core, the floor, and the next action (update spt-core first). The check is a numeric per-component compare (so `0.9.0 < 0.25.0`), and it fires **before** anything is written: a refused add leaves the registry untouched, and a refused update leaves the live install byte-untouched. The enforcement is unconditional — there is **no** opt-in flag (the former `[update].version_check` knob w B) REQ-HAZARD-DEFERRED-MANIFEST Title: 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) Doc: PAIR 65: REQ-ADAPTER-MULTIPLATFORM-SPT <-> REQ-CI-POSTJOB-DAEMON-REAP ================================================================================ A) REQ-ADAPTER-MULTIPLATFORM-SPT Title: A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `/` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2) Doc: Multi-platform adapter `.spt` packaging B) REQ-CI-POSTJOB-DAEMON-REAP Title: A CI job REAPS ITS OWN test-spawned daemons at battery end, in-job, and logs a process census at job start AND job end so contamination and reap effectiveness are visible in every run's log. (Load-flake family leg 1, doyle-ratified 2026-07-22 from deployah's third-run analysis.) THE SIGNATURE THIS CLOSES: a DIFFERENT single daemon-spawning test dying per run with a bare exit 1 and NO assertion output — process-level death, not a failed assert — while sibling tests in the same families pass alongside it, on BYTE-IDENTICAL code. Evidence: release PR #56 ran four times over a zero-.rs-delta tree; runs 1/2/3 killed brain_decouple (twice, on a disk-starved box), then adapter_translate, then adapter_digest at 105.9 GB free; box census during runs showed 43 live spt-family processes and 6486 handles against an 1881-test Phase-A full-parallel battery; run 4 went GREEN once disk and leaked session-0 daemons were cleared. WHY IN-JOB IS LOAD-BEARING AND NOT A CONVENIENCE: there are TWO leak populations on hfenduleam. Population A is session-1 (agent/gate-spawned) and is sweepable by path from any shell. Population B is SESSION-0, spawned by the actions.runner.* service — a session-1 shell CANNOT kill those (Access denied; ExecutablePath unreadable) even though they are healthy. Every CI run therefore leaves session-0 daemons behind that contend with the NEXT run while also leaking its own mid-run, and an external sweep can never reach them. The runner's own job context owns its session-0 children, so only a post-job step inside the job can reap them. KILL SCOPE IS NARROW AND PATH-VERIFIED PER-PID AT KILL TIME (never machine-wide, and never trusting the census snapshot, whose pids can be recycled): eligible only under the run's own build roots — CARGO_TARGET_DIR, the workspace target, the notify-adapter checkout's target, RUNNER_TEMP, and the pinned n1 old-broker build cache. TWO HARD EXCLUSIONS are checked AFTER the root test rather than instead of it, so that live infra survives a bug in the root computation: anything under an spt-core/bin/ install prefix, and any owl binary. A process whose image path cannot be READ is reported but NEVER killed — unreadable means unverifiable and the safe direction is to leave it standing. A BOUNDED SETTLE precedes the kill pass so a cleanly-exiting daemon is not counted as a leak; after it, a survivor is a leak by definition, which is what makes the strict-mode trigger a mechanism rather than a judgement call. Rides BOTH the test and n1-gate jobs (doyle scope ruling): n1_pairing spawns real daemon trees from the workspace and from the out-of-tree pin cache, so reaping one job leaves half the cause standing. Gate: impl — the two census/reap scripts under .github/ci/ plus their job-start and always() job-end wiring on both jobs, both runners. Kin REQ-CI-WINDOWS-PHASE-A-BOUND (the other cause-side leg), REQ-CI-DOCS-ONLY-THIN (recipe-layer precedent: impl-only, no product code). Doc: PAIR 66: REQ-ADAPTER-UPDATE-INPLACE <-> REQ-UPDATE-GH-TRANSPORT ================================================================================ A) REQ-ADAPTER-UPDATE-INPLACE Title: Bug #18: spt adapter update fails at re-register with os error 2 because it derives the install dir from the update repo NAME (_github/) instead of updating in place at the adapter record source_dir; when the adapter repo is intentionally renamed across releases (spt-claude-code to claude-spt, supported), the derived dir is fresh/empty and re-register reads a missing manifest. Fix: adapter update installs and re-registers in place at the registered source_dir and tolerates a changed update repo/URL across a rename. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #18. Doc: B) REQ-UPDATE-GH-TRANSPORT Title: THE-FORKENING W1 (ADR-0036, operator-ruled 2026-07-14): the release channel is PRIVATE (`BigscreenVR/spt-bs-releases`) and the gh CLI is the mandated carrier — release discovery (`releases/latest`, cli.rs:9717) and asset download (cli.rs:4861 public browser URLs) move to deadline-wrapped `gh` subprocess calls (`gh api`, `gh release download`; run_git pattern). WHY gh not token+HTTP: private-repo `browser_download_url` 404s even with a valid token — the API asset-id dance is gh's job. Default repo flips via the existing SPT_INSTALL_REPO seam (cli.rs:5363) + xtask REPO const (main.rs:729) + notif.rs consent-changelog URL rider. Loud failure classes: gh missing -> UPDATE_FETCH_REJECTED:GhCliRequired with OS-SPECIFIC install hints (winget/apt/brew); gh unauthed -> distinct GhAuthRequired pointing at `gh auth login`. Signature verification unchanged — bytes verified after download, carrier-independent (update-set/counter/anchor continuity per ADR-0036 §2). release_verify_e2e reworked to the gh carrier. Gate: unit — url/invocation construction + both failure classes render OS-correct hints; int — fetch against a real gh-authed channel resolves latest + downloads and verifies an asset; doc — self-update docs name the gh prerequisite. Kin REQ-INSTALL-BOOTSTRAP-VERB (same carrier at first install), ADR-0036. Doc: release channel (private, gh-carried)** — the release channel is a **private** GitHub repo (`BigscreenVR/spt-bs-releases`, ADR-0036); the **gh CLI is the mandated carrier** for release discovery and asset download (each node authenticates via org membership). A node without an authed `gh` cannot fetch — refused loud with OS-specific install hints, never a silent hang. Signature verification is carrier-independent: bytes are verified after download exactly as before; counter, signing key, and update-set format are unchanged from the public-channel era. / Prerequisite: the GitHub CLI.** The rele PAIR 67: REQ-API-4 <-> REQ-UPDATE-FETCH-CURRENT-UX ================================================================================ A) REQ-API-4 Title: 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) Doc: Manifest resolution from `--adapter` (since v0.8.0).** `spt api --adapter ` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest ` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter B) REQ-UPDATE-FETCH-CURRENT-UX Title: `spt update fetch` reports an already-staged / already-applied latest as an ACTIONABLE human outcome (exit 0), not a Debug-formatted error. ROOT: cmd_update_fetch (cli.rs) sets the rollback floor = staged_version, so when the published candidate == the already-staged version, verify_update_set_metadata returns Err(RejectReason::Rollback{current,candidate}) — printed as {reason:?} (Debug) at exit 1, reading as a FAILURE when the update is merely already downloaded and just needs `spt update apply` (this bit the operator: fetch kept 'failing', apply was the missing step). FIX: a PURE classifier (reason, applied, staged) -> {AlreadyStaged (latest downloaded, not yet installed) / AlreadyApplied (up to date) / GenuineError}; already-staged + already-applied print a friendly message and exit 0; genuine rejects use RejectReason's Display (release.rs, not Debug) + exit 1 — applied at ALL THREE fetch reject sites (metadata + artifact-verify + plan-verify). (v0.18.0) Doc: PAIR 68: REQ-DAEMON-9 <-> REQ-DAEMON-SERVICE-INSTALL ================================================================================ A) REQ-DAEMON-9 Title: 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. Doc: B) REQ-DAEMON-SERVICE-INSTALL Title: F-038 RIDER (flynn nice-to-have, QUEUED not activated): a documented OS-service registration recipe or `spt daemon install-service` verb so the daemon itself survives box reboot (flynn's box runs managed_by:null = daemon-at-boot unprovisioned; kitsubito's hand-rolled systemd --user unit = prior art). NOT in MSG-IDENTITY scope — REQ-ENDPOINT-AUTOSTART covers the daemon-start-to-endpoint leg; the boot-to-daemon leg stays interim (logon scheduled task / systemd unit). Activate at an infra-provisioning milestone; shape (recipe doc vs verb) ruled then. Kin [[daemon-service-detection-gotcha]] (global-OS-state detection blind on dev box — a verb must not regress that), [[kitsubito-linux-rig]]. Doc: PAIR 69: REQ-EP-9 <-> REQ-INST-13 ================================================================================ A) REQ-EP-9 Title: `#` 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). Doc: Status: accepted (2026-06-21) B) REQ-INST-13 Title: Subnet-exclusive sync + per-endpoint subnet-membership list Doc: PAIR 70: REQ-HAZARD-DETACHED-DAEMON-STDIO <-> REQ-INSTALL-11 ================================================================================ A) REQ-HAZARD-DETACHED-DAEMON-STDIO Title: A daemon DETACHED-IN-FACT (no interactive console, or an inherited stderr PIPE nobody drains) that never nulled its std handles will BLOCK on stdio writes when the pipe fills, and/or pop a visible conhost window (REQ-HAZARD-WMI-DAEMON-WINDOW is a covered surface of this hazard). detach_console nulls the 3 handles only under the --detached flag; a rung that omits it (the bare line-82 elevated->deelevated respawn; a STALE installer at-logon task registered as bare `daemon run`, confirmed live field-drift on ENLYZEAM) is exposed. FIX: (load-bearing) inside `daemon run`, null the 3 std handles when stderr GetFileType==FILE_TYPE_PIPE — a pipe is the ONLY std sink that BLOCKS the daemon when it fills; catches every rung whose inherited stderr is an undrained pipe, independent of whether each caller passed --detached, while a FILE (2>run.log AND every int-test Stdio::from(file) brain-log capture), a CONSOLE (scrolls), and a NULL/absent handle (DETACHED_PROCESS rungs, already discard) all SURVIVE. DELIBERATELY NOT gated on GetConsoleWindow==NULL: a CREATE_NO_WINDOW daemon has no console window yet a drained FILE stderr — nulling it would blank the capture for ZERO safety gain (a file never blocks) and mass-red the int-test brain-log assertions. (belt) pass --detached on the bare line-82 respawn; (drift nag) parse the schtasks /Query action argv and LOUDLY nag when the at-logon task is the stale bare `daemon run` form (manual/installer re-registers; the daemon must NOT self-elevate to rewrite it). Defense-in-depth — no current spt Windows spawn path was proven to yield a BLOCKING inherited pipe (all rungs null-discard or scrolling-conhost), so this is hardening, not a confirmed incident root. Doc: B) REQ-INSTALL-11 Title: 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) Doc: Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it. A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github//` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binari PAIR 71: REQ-INST-1 <-> REQ-RUN-SHORTCUT ================================================================================ A) REQ-INST-1 Title: endpoint ID vs instance split (adapter-agnostic ID) Doc: B) REQ-RUN-SHORTCUT Title: `-` 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). Doc: `spt-` shortcut** (picker `s` keybind, M12-W2): From any pre-start options set, `s` writes (or updates) a **`-` launcher** at the project root that bakes the current selection's **non-interactive** flags (terminal actions only: adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches — Kick/Instantiate/Change-adapter/Fork — are not bakeable). The **basename is a parameter**: harness-agnostic spt-core defaults to **`spt`** (→ `spt-`, e.g. `spt-doyle`); an adapter/flow **overrides** it (spt-claude-code → `cc`, giving `cc-`) — the Claude- PAIR 72: REQ-INST-12 <-> REQ-INSTALL-6 ================================================================================ A) REQ-INST-12 Title: Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync Doc: B) REQ-INSTALL-6 Title: 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) Doc: PAIR 73: REQ-INST-15 <-> REQ-INSTALL-11 ================================================================================ A) REQ-INST-15 Title: 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) Doc: Delivered (M4-D9-5, 2026-06-04):** home assignment at creation (`spt_store::home` — auto-if-one / ask-if-many / local-only-until-first-join, carried forward across re-binds, no setter) + `sync_subnets = [home]` creation seeding + `spt fork --subnet / 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) Doc: Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it. A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github//` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binari PAIR 74: REQ-INST-4 <-> REQ-INSTALL-7 ================================================================================ A) REQ-INST-4 Title: active to dormant/suspended fires a transition echo commune Doc: B) REQ-INSTALL-7 Title: 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) Doc: PAIR 75: REQ-INST-7 <-> REQ-UPDATE-GH-TRANSPORT ================================================================================ A) REQ-INST-7 Title: Subnet registry + bare-id resolution policy Doc: B) REQ-UPDATE-GH-TRANSPORT Title: THE-FORKENING W1 (ADR-0036, operator-ruled 2026-07-14): the release channel is PRIVATE (`BigscreenVR/spt-bs-releases`) and the gh CLI is the mandated carrier — release discovery (`releases/latest`, cli.rs:9717) and asset download (cli.rs:4861 public browser URLs) move to deadline-wrapped `gh` subprocess calls (`gh api`, `gh release download`; run_git pattern). WHY gh not token+HTTP: private-repo `browser_download_url` 404s even with a valid token — the API asset-id dance is gh's job. Default repo flips via the existing SPT_INSTALL_REPO seam (cli.rs:5363) + xtask REPO const (main.rs:729) + notif.rs consent-changelog URL rider. Loud failure classes: gh missing -> UPDATE_FETCH_REJECTED:GhCliRequired with OS-SPECIFIC install hints (winget/apt/brew); gh unauthed -> distinct GhAuthRequired pointing at `gh auth login`. Signature verification unchanged — bytes verified after download, carrier-independent (update-set/counter/anchor continuity per ADR-0036 §2). release_verify_e2e reworked to the gh carrier. Gate: unit — url/invocation construction + both failure classes render OS-correct hints; int — fetch against a real gh-authed channel resolves latest + downloads and verifies an asset; doc — self-update docs name the gh prerequisite. Kin REQ-INSTALL-BOOTSTRAP-VERB (same carrier at first install), ADR-0036. Doc: release channel (private, gh-carried)** — the release channel is a **private** GitHub repo (`BigscreenVR/spt-bs-releases`, ADR-0036); the **gh CLI is the mandated carrier** for release discovery and asset download (each node authenticates via org membership). A node without an authed `gh` cannot fetch — refused loud with OS-specific install hints, never a silent hang. Signature verification is carrier-independent: bytes are verified after download exactly as before; counter, signing key, and update-set format are unchanged from the public-channel era. / Prerequisite: the GitHub CLI.** The rele PAIR 76: REQ-INSTALL-10 <-> REQ-UPDATE-FETCH-APPLY-FLAG ================================================================================ A) REQ-INSTALL-10 Title: 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) Doc: B) REQ-UPDATE-FETCH-APPLY-FLAG Title: `spt update fetch --apply` is the one-shot get-to-latest: fetch, then INSTALL the staged update REGARDLESS of whether the fetch itself staged anything new — so the brittle `fetch && apply` chain (which broke when fetch no-oped / exited nonzero on an already-staged latest, skipping the chained apply) is unnecessary. Composes with REQ-UPDATE-FETCH-CURRENT-UX: the end state is 'installed latest', reached idempotently from new-staged -> apply / already-staged (applied STILL apply / already-applied -> noop+exit0 / genuine error (bad signature, no artifact for platform, true downgrade, network) -> do NOT apply, propagate the error + nonzero. Reuses the existing cmd_update_apply core (its own verify + two-phase + auto-rollback own correctness; no duplicated swap/respawn). Additive clap flag (plain doc-comment, no internal codes); reference.md regenerated. (v0.18.0) Doc: PAIR 77: REQ-INSTALL-3 <-> REQ-PLATFORM-REGISTRY ================================================================================ A) REQ-INSTALL-3 Title: Idempotent + interactive-optional first run Doc: B) REQ-PLATFORM-REGISTRY Title: MUSL-TIER W1 (target-triple centralization, behaviour-NEUTRAL refactor): ONE authoritative platform registry from which current_platform(), KNOWN_TARGET_TRIPLES, the applyhost cross-platform 'other' logic, and the asset-name<->triple map all derive. ROOT: the target triple x86_64-unknown-linux-gnu + the implicit 'exactly 2 platforms' assumption are hardcoded across ~6 sites (release.rs current_platform cfg + KNOWN_TARGET_TRIPLES, applyhost.rs:740-743 win/linux binary if/else, xtask asset map, release.yml), so adding any platform (musl, future arm64) is a scattered edit. FIX: a data-driven registry (candidate: SUPPORTED_PLATFORMS const table of {triple, asset_name}) + generalize applyhost 'other' to 'every registered platform except current_platform()'. gnu+windows behaviour BYTE-IDENTICAL — the existing release/update/apply/propagate suites stay green (that is the gate). DESIGN FORK (doyle rules pre-dispatch): enum vs const-table; applyhost N-platform generalization; current_platform stays cfg->triple but output must be a registry member, loud 'unknown' fallback kept. Doc: PAIR 78: REQ-MANIFEST-SUBST <-> REQ-PICKER-ADAPTER-DESCRIPTION ================================================================================ A) REQ-MANIFEST-SUBST Title: Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.16.0) Doc: Adapter-static keys — `{adapter_dir}` and `{adapter_name}` (since v0.16.0).** Two of the catalog keys are *adapter-static* — they depend only on the resolved adapter, never on a session or event, so they are available **wherever** command/string substitution runs (every `[session.*]` template, the `[digest]` extractor, the `[message-idle-translation-binary].command`, and — uniquely — inside `[strings]` values at `get-string` read time): / Lazy substitution inside `[strings]` values (since v0.16.0).** A `[strings]` value (inline literal or the read-back contents of a pointer file) may contain t B) REQ-PICKER-ADAPTER-DESCRIPTION Title: 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) Doc: PAIR 79: REQ-ADAPTER-UPDATE-POST <-> REQ-BROKER-OUTPUT-BEFORE-EXIT ================================================================================ A) REQ-ADAPTER-UPDATE-POST Title: Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0) Doc: `[update.post]` — the composite post-step (since v0.16.0) / Composite update — `[update.post]` (since v0.16.0).** An optional **avenue-agnostic** sub-table that runs a delegated **post-step** *after* the primary update avenue resolves, in the same `spt adapter update`. It lets an adapter pull its `.spt` from `gh_release` **and** run a second, adapter-owned step (e.g. an in-harness plugin sync) under one lever. B) REQ-BROKER-OUTPUT-BEFORE-EXIT Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 1, hertz stale-glyphs RCA leg 1 P0): the PTY drain/output writer is the SOLE FIFO sequencer for terminal Output + Exit per attach sink — Exit is enqueued BEHIND all prior output (drain EOF/completion first, then Exit); the exit waiter never direct-writes KIND_EXIT around the queued output path (a mutex serializes bytes, not producer order). Kills the stranded-final-frame race (final EL/SGR-reset/cursor-show/?1049l lost when Exit overtakes Output — already admitted and compensated in the broker test suite, never fixed in production rc). Gate: impl — single sequencer, exit-behind-output enqueue; unit — ordering invariant on the writer queue (exit never precedes queued output for a sink); int — short-lived child emits 'XXXX ESC[2K ESC[?25h ESC[?1049l' then exits => that exact Output precedes Exit through the PRODUCTION broker->attach->rc path; doc — ADR-0043. Doc: 1. **One FIFO sequencer per attach sink.** The PTY drain/output writer is the sole sequencer for terminal Output and Exit: Exit is enqueued behind all prior output for each sink (drain EOF/completion first, then Exit). A mutex alone is insufficient — producer order is the contract. Output-before-Exit is a production-path invariant, regression-proven end-to-end (broker → attach → rc). 2. **rc display teardown is unconditional, idempotent, and separate from input teardown. A display RAII guard (distinct from the OS input/raw-mode guard) runs on every exit path including errors and unwind: best-e PAIR 80: REQ-HAZARD-ATOMIC-TMP-COLLISION <-> REQ-HAZARD-SINGLE-PATH-SOURCE ================================================================================ A) REQ-HAZARD-ATOMIC-TMP-COLLISION Title: Concurrent atomic writers to the same target must not share a tmp name (5.15): a fixed tmp sibling makes one writer's rename consume the other's staged file (os-error-2 loser) Doc: 5.15 Fixed atomic-write tmp name → concurrent writers collide (loser renames a consumed file) `[REQ-HAZARD-ATOMIC-TMP-COLLISION]` Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: ` B) REQ-HAZARD-SINGLE-PATH-SOURCE Title: Single path/registry source of truth; no layout ambiguity (6.1) Doc: PAIR 81: REQ-HAZARD-INFO-JSON-TORN-READ <-> REQ-PROJECT-INDEX-INVALIDATION ================================================================================ A) REQ-HAZARD-INFO-JSON-TORN-READ Title: State-file reads tolerate concurrent writes (1.2) Doc: B) REQ-PROJECT-INDEX-INVALIDATION Title: PROJECT-INDEX W2 (ADR-0037): event-driven index invalidation, debounced + coalesced: new session/cwd -> refresh that endpoint + the shared cwd cache; context-store mutation -> ONE debounced global membership refresh keyed on BRANCH-TIP FINGERPRINTS (no authoritative context-commit path exists — a writer-maintained generation counter is REJECTED because nothing maintains it); bind/start, rename, fork, purge -> affected endpoint rows; low-frequency periodic reconcile as the backstop; cwd identity refresh ONLY on path/repo-identity/.git-config change (ordinary commits do NOT alter project identity); multiple invalidations coalesce into one refresh. Gate: impl — the event set + debounce/coalesce; unit — each event class maps to its refresh scope + coalescing proof + ordinary-commit no-op; int — session/context/rename/fork/purge invalidation against a live daemon; doc — rides the ADR + triage doc. Kin REQ-PROJECT-INDEX-WRITER. Doc: index writer duty (daemon)** — the brain hosts ONE writer thread (`projwriter`, spawned beside the live host; single-flight by construction). Batched complexity is contract, `O(P+B+F+C)`: ONE branch enumeration per cycle (`for-each-ref` carries recency + tips), ≤1 tree scan per **changed** `p-*` branch (`ls-tree` at tip, membership cached by tip), ONE derivation per distinct normalized cwd (cache stamped on the repo-identity marker — `.git/config` / the `.git` gitfile — so **ordinary commits are a no-op by construction**); backgrounding the legacy 100+ process loop is REJECTED. Cold start: dae PAIR 82: REQ-HAZARD-STORE-INIT-RACE <-> REQ-PROJECT-INDEX-STORE ================================================================================ A) REQ-HAZARD-STORE-INIT-RACE Title: Concurrent first-touch of ONE fresh BranchStore must ALL succeed, never a hard error. BranchStore::open_or_init (spt-store/src/branchstore.rs:47) is a TOCTOU: it gates on HEAD.exists() then runs a NON-ATOMIC init (`git init --bare` + `git config core.autocrlf false` + best-effort worktree.useRelativePaths). Two processes that both observe !HEAD.exists() on one fresh store race the `git config` step, which takes git's per-repo config.lock — the loser fails with 'could not lock config file …/config: File exists', an io::Error that strands the caller (the G3-gate pump.rs:442 flake, doyle-ledgered). FIX: make init race-tolerant — `git init --bare` is idempotent, and `git config` is idempotent (same bytes), so tolerate a concurrent winner (open-after-lose: if init errors but HEAD now exists, proceed as opened) and retry a transient config.lock collision a bounded number of times so the required core.autocrlf=false is guaranteed set. N concurrent open_or_init on ONE fresh dir must ALL return Ok. (F-025 wave, doyle Item 2) Doc: 7.26 Concurrent first-touch of ONE fresh BranchStore must ALL succeed — a non-atomic `git init` race must never strand a first-toucher `[REQ-HAZARD-STORE-INIT-RACE]` Failure (G3-gate pump.rs:442 flake, doyle-ledgered; clean-room repro 2026-07-02):** `BranchStore::open_or_init` (branchstore.rs:47) is a TOCTOU — it gates on `HEAD.exists()` then runs a NON-ATOMIC init (`git init --bare` + `git config core.autocrlf false` + best-effort `worktree.useRelativePaths`). N processes that all observe `!HEAD.exists()` on ONE fresh store race the init: concurrent `git init --bare` collide copying template B) REQ-PROJECT-INDEX-STORE Title: PROJECT-INDEX W1 (ADR-0037, RCA .claude/reports/2026-07-10-hertz-session/03): spt-store owns the VERSIONED materialized project-index format + read path. Reader contract: read one compact versioned index, join with the local perch roster, return immediately; stale/missing renders last-known-good or '-'; NEVER fall back to synchronous git enrichment; daemon-offline readers consume the last persisted snapshot; truncated/schema-mismatched index degrades to fast reads + last-known-good, never an error stall. Gate: impl — format + store read path; unit — version/schema-mismatch/truncation degradation legs + join semantics; doc — CONTEXT.md project-index entry + STORAGE.md section. Kin REQ-PROJECT-INDEX-WRITER (the producer), ADR-0037. Doc: project index** — a node's endpoint→project attribution is DERIVED state, held as a **persistent materialized index** (ADR-0037): `spt-store` owns the versioned format + read path (daemon-offline reads = last persisted snapshot); the **daemon is the sole single-flight writer** (load-at-start, ready-without-warm, background batched reconcile, atomic replace, coalesced event-driven invalidation keyed on branch-tip fingerprints, last-known-good on failure). Readers — list, picker, endpoint-info, hooks — join index × perch roster and **never run git**; stale renders last-known or `-`, never a stal PAIR 83: REQ-MANIFEST-3 <-> REQ-PROJECT-INDEX-WRITER ================================================================================ A) REQ-MANIFEST-3 Title: 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) Doc: adapter strings** (ratified 2026-06-11, Gateway grill): A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string ` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in B) REQ-PROJECT-INDEX-WRITER Title: PROJECT-INDEX W2 (ADR-0037): the daemon is the SOLE single-flight project-index writer: load persisted index at startup; ready WITHOUT warm (cold start = daemon ready + CLI fast before background completes); background reconcile with BATCHED complexity O(P+B+F+C) — enumerate branches ONCE, <=1 tree scan per changed branch, ONE derivation per distinct normalized cwd (in-process BranchStore traversal or fixed plumbing calls; backgrounding the existing 100+ process loop is REJECTED); atomic replace; last-known-good preserved on any failure; warm start with unchanged generation performs NO scan. Observability surface: generated time, source generation, pending refresh, last duration/error, endpoint/project/cwd counts, cache hits/misses, stale reads, repair count — index presence alone is not health. Gate: impl — writer + observability; unit — single-flight, atomic-replace, last-known-good, no-scan-on-unchanged-generation; int — cold+warm start legs against a real store; COMPLEXITY COUNTERS are the CI gate (wall-clock = manual acceptance ONLY, shared-runner flake class); doc — daemon docs writer-duty section. Kin REQ-PROJECT-INDEX-STORE, REQ-PROJECT-INDEX-INVALIDATION. Doc: index writer duty (daemon)** — the brain hosts ONE writer thread (`projwriter`, spawned beside the live host; single-flight by construction). Batched complexity is contract, `O(P+B+F+C)`: ONE branch enumeration per cycle (`for-each-ref` carries recency + tips), ≤1 tree scan per **changed** `p-*` branch (`ls-tree` at tip, membership cached by tip), ONE derivation per distinct normalized cwd (cache stamped on the repo-identity marker — `.git/config` / the `.git` gitfile — so **ordinary commits are a no-op by construction**); backgrounding the legacy 100+ process loop is REJECTED. Cold start: dae PAIR 84: REQ-PROJECT-INDEX-READER-CUTOVER <-> REQ-WHOAMI-IDENTITY-ONLY ================================================================================ A) REQ-PROJECT-INDEX-READER-CUTOVER Title: PROJECT-INDEX W3 (ADR-0037): endpoint list, the picker, and endpoint-info consume the materialized index — NO git work in any user-facing read path (the O(PxB+C) fanout at cli.rs ~2954 / picker/data.rs ~456-518 dies). BEHAVIORAL PARITY is binding: precedence session-cwd -> origin-cwd -> context-recency and rendered project IDs/display names unchanged (parity suite vs the old derivation on a fixture); bare/partial run shares the indexed projection; fully-qualified --adapter+--id direct run stays picker-free; the direct-run 25s broker-session gate stays separately tested/observable. Degradation legs (git unavailable, branch malformed/locked, cwd deleted) keep fast reads. Manual latency acceptance on the 13-perch/7-branch fixture (~30s -> sub-second) + hertz field-verify on HFENDULEAM — NOT a CI wall-clock gate. Gate: impl — reader cutover; unit — parity + degradation; int — list/picker against a daemon-maintained index incl. counters proving zero reader git spawns; doc — reference regen + CONTEXT avoid-list. Kin REQ-WHOAMI-IDENTITY-ONLY, REQ-PROJECT-INDEX-STORE/WRITER/INVALIDATION. Doc: project index** — a node's endpoint→project attribution is DERIVED state, held as a **persistent materialized index** (ADR-0037): `spt-store` owns the versioned format + read path (daemon-offline reads = last persisted snapshot); the **daemon is the sole single-flight writer** (load-at-start, ready-without-warm, background batched reconcile, atomic replace, coalesced event-driven invalidation keyed on branch-tip fingerprints, last-known-good on failure). Readers — list, picker, endpoint-info, hooks — join index × perch roster and **never run git**; stale renders last-known or `-`, never a stal B) REQ-WHOAMI-IDENTITY-ONLY Title: PROJECT-INDEX W1 (F-040, perri filing claude-spt docs/SPT-CORE-FINDINGS.md @d775b38; correctness-critical opener — the 2026-07-15 message-bodies incident root): a core IDENTITY-ONLY resolution — session -> endpoint|null — that touches NO list/registry/project/git/network path, and `spt whoami` DE-ALIASED from cmd_endpoint_list (cli.rs ~6609 aliases the full list = 100+ git children under hook deadlines). endpoint-info is DISQUALIFIED as the carrier (runs latest_project_ref). Adapters/hooks get a bounded-time identity verb; the harness-hosted adapter fallback stays deadline-vulnerable until this ships. Gate: impl — the resolver + whoami de-alias; unit — resolver returns endpoint|null with zero project derivation (assert no git spawn seam); int — whoami on a multi-perch home answers fast-path without touching context branches; doc — harness-contract api.md names the identity verb + its no-derivation bound. Kin REQ-PROJECT-INDEX-READER-CUTOVER (list-shaped verbs), REQ-WHOAMI-1, docs/PROJECT-INDEX-TRIAGE.md. Doc: The bounded-time "which endpoint am I?" answer for hooks and adapter glue: resolves the calling session to its endpoint (`$OWL_SESSION_ID` `$SPT_AGENT_ID` / process ancestry) and prints that ONE endpoint's SELF line — id, liveness, description. **The no-derivation bound is the contract**: whoami never enumerates the roster, never derives projects, never runs git, never touches the network — safe to call from deadline-bounded hook paths (the class that previously timed out and black-holed message delivery). Unresolved is a clean answer, not an error stall: `NO_PERCH` on stderr (`--json`: `{"id" PAIR 85: REQ-HAZARD-DELIVERY-STARVATION <-> REQ-SPOOL-TAKE-AUDIT ================================================================================ A) REQ-HAZARD-DELIVERY-STARVATION Title: A NON-DEFERRED message that has REACHED a node's spool (WAN-arrived or locally spooled-while-active) is NEVER dependent on an adapter HOOK-POLL cadence for its eventual delivery to an spt-hosted (relay-less) endpoint — the daemon itself drives delivery on the events it owns (WAN ingress + the ACTIVE→IDLE edge). Hazard class: delivery starvation. Without this, cross-node and post-active messages to an spt-hosted perch strand indefinitely whenever the adapter's hooks are quiet (idle session, no user turns), presenting as 'sent but never lands' with a healthy binary and an idle perch (F-023). SCOPE NARROWED 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling): the deferred (active_only) class is EXPLICITLY OUTSIDE this guarantee — per ADR-0028 it is hook-carried background context that never wakes an idle agent; with no hook cadence it waits by design. The no-starvation guarantee binds the default and idle_only classes only. Guarded by REQ-WAN-SPT-HOSTED-DELIVERY (WAN ingress leg) + REQ-MSG-IDLE-EDGE-DRAIN (idle-edge drain, as amended). (F-023) Doc: ⚠ REVERSED 2026-07-27 — the collapse this document argued AGAINST is now SHIPPED Read this banner before treating anything below as current.** The 2026-07-09 ruling recorded here — that the idle edge SHOULD carry `active_only` for the relay-less spt-hosted class, and that the active/idle asymmetry was *intentional* — was **reversed by operator ruling in the FIELD-TRUTH W1 roll-in.** The idle-edge and parked-re-offer claims now exclude `deferred = 1` **unconditionally**, on every endpoint class. That is precisely the "collapse" this document was written to block. What changed was the evidence, B) REQ-SPOOL-TAKE-AUDIT Title: W5 (LIFECYCLE-TRUTH, RCA cost: proving WHO took delivered=1 rows burned an hour): the spool records the taker per row — leg enum (relay-backlog / hook-poll / idle-inject / psyche) + sid/pid + taken_at ms — surfaced by a --json debug read. Additive column, no schema break (delivered rows already retained). Doc: PAIR 86: REQ-MSG-DELIVERY-AXES <-> REQ-RELAY-NO-BUSY-DELIVER ================================================================================ A) REQ-MSG-DELIVERY-AXES Title: Activity-gated inbound delivery + per-message send control as THREE ORTHOGONAL AXES plus opaque metadata (ADR-0028; grilled w/ operator 2026-06-23). SUBSTRATE (the legacy-SPT parity gap, scaffolded-but-unwired today: `delivery::is_idle` + `resolve_inject_methods` exist but the result is discarded `let _methods`, and `broker::dispatch_endpoint_input` injects unconditionally — its comment calls activity-gating 'a deferred follow wave'): an inbound message has an ACTIVE window (endpoint active → spool for the receiver's hook-poll, non-disruptive) and an IDLE window (idle/idle-transition → deliver immediately: translation binary spt-hosted → relay-poll either topology → spool, in fallback order). AXES (each composes; each defaults to its unrestricted value): (1) DELIVERY WINDOW — default (both, first-to-fire) | `--idle-only` (idle window; immediate if already idle) | `--active-only` (active window only, never wakes; the RENAMED `--deferred` — `deferred=1` spool column + `api poll --include-deferred` keep their names). (2) CHANNEL RESTRICTION — unrestricted | `--prefer-native` (translation binary if running else fall back) | `--force-native` (binary ONLY, no fallback/no spool-to-other-method). Native flags do NOT respect the binary's idle-gating: the WINDOW says when, the native flag says through-what (so `--force-native --active-only` = binary injects during the active window, mid-turn-safe via the existing InjectFloor). (3) PERSISTENCE — durable (default; spool until delivered or TTL) | `--ephemeral` (drop if undeliverable in the accepted window — at window-open with no live carrier, or at TTL, whichever first). METADATA (orthogonal): `--json-payload ''` → a single attr-escaped `json="…"` envelope attr ALONGSIDE (not replacing) the body, pure verbatim passthrough across spool/TCP/WAN/EVENT-PART, parsed only by the receiving adapter; collision-proof by construction (structured data lives INSIDE the one `json` value, can never forge `from`/`type`); available to ANY sender (confers no spt-core authority). HAZARD: `--ephemeral` is the ONLY path permitted to drop silently — the sender-opted-in carve-out to REQ-HAZARD-IDLE-SILENT-NONDELIVERY (that hazard gains a '…unless --ephemeral' clause in v0.15.0). (v0.15.0) Doc: Activity-gated message delivery + send-modifier axes B) REQ-RELAY-NO-BUSY-DELIVER Title: MSG-IDENTITY W4 (operator self-send probe 2026-07-09, companion leg): the relay/idle-inject carrier MUST NOT fire for an ACTIVE endpoint — the daemon already guards send-time (daemon.stderr.log: 'ENDPOINT_INJECT: endpoint ACTIVE -> spool (deferred hint), not injected') yet the field row ended taken_leg='idle-inject' for a message sent while the endpoint was ACTIVE, so the guard is bypassed somewhere on the busy-to-idle EDGE (the parked-drain idle-injects rows that arrived during busy without re-checking whether a poll is concurrently draining them — the edge itself is when BOTH carriers are plausibly live). RCA-FIRST with REQ-CARRIER-CLAIM-EXCLUSIVE (same rig, same instrument); if the exclusive claim alone closes the double-delivery this leg may reduce to an ordering assertion — rule at RCA lock, don't build blind. Gate: unit — the idle-edge drain re-verifies activity (or defers to the claim) before idle-injecting; a row taken by a poll is never idle-injected. Kin REQ-IDLE-PARKED-DELIVERY (the drain this guards), REQ-MSG-IDLE-EDGE-DRAIN, F-023 anti-starvation gate (do NOT break the already-idle delivery class). Doc: PAIR 87: REQ-CONN-POISON-DIAL-SCOPE <-> REQ-SHELL-4 ================================================================================ A) REQ-CONN-POISON-DIAL-SCOPE Title: MSG-IDENTITY W6 / F-039 (deployah field-acceptance follow-up 2026-07-10, RCA-FIRST — mint per the v0.30.6 PASS handoff): ambient CONN_WRITE_POISONED log-churn correlates 1:1 with PUMP_PEER_FAIL submit-dials to OFFLINE peers (enlyzeam/kitsubito/gravity) with NO wedge and NO freeze — pre-existed the blackhole rig = log-noise/mislabel, not a defect in the r4 fix. CODE CONTEXT: conn.rs poison_and_cancel emits the loud CONN_WRITE_POISONED line for a write that 'exceeded its bound (OR FAILED)' (conn.rs:181) — the fast-FAIL branch (broken pipe / conn refused on an already-dead counterpart) shares the log tag with the TIMEOUT branch that is the field-acceptance wedge observable, so routine conn teardown under offline-peer dial churn reads like poison events. RCA-FIRST: pin the exact write site that fails per PUMP_PEER_FAIL cycle (BrokerConn is broker-side — which broker conn write rides each pump dial failure? status/event fan-out to a departed subscriber? brain-side notification?) BEFORE changing anything — the correlation mechanism is unpinned. FIX SHAPE (post-RCA, doyle rules at lock): reserve the loud CONN_WRITE_POISONED token for the DEADLINE-EXCEEDED class (the wedge observable blackhole-controller.ps1 watches); a plain write-FAIL on an already-dead conn retires quietly (debug-level or a distinct low-noise token). MUST NOT weaken the r4 invariant: every failure path still poisons + retires the conn (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK behavior unchanged — only the log LOUDNESS is scoped). Gate: unit — deadline-exceeded emits the loud token, fast-fail does not (both still poison); field — offline-peer churn no longer floods daemon.stderr.log with CONN_WRITE_POISONED. Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant this must preserve), REQ-PUMP-PEER-ISOLATION (the offline-peer dial neighborhood), [[v0306-published]]. Doc: B) REQ-SHELL-4 Title: 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). Doc: shell tunnel: a long-lived reliable-ordered link-bound QUIC stream pair carrying opaque bytes the taxonomy never reinterprets; manifest opt-in, not enveloped/MAC-framed/spooled; link-break closes it; reliable-ordered ⇒ on-LAN posture Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**: a long-lived, **reliable, ordered** byte stream (a dedicated QUIC stream pair bound to the link) for protocol traffic the channel taxonomy must NOT reinterpret — opaque wire protocols spoken end-to-end (first consumer: USB/IP URB traffic t PAIR 88: REQ-HAZARD-ID-CHARSET <-> REQ-OPID-MINTER-NAMESPACE ================================================================================ A) REQ-HAZARD-ID-CHARSET Title: Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6) Doc: 4.6 Addressable-id charset reserves the address delimiters Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration. Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start B) REQ-OPID-MINTER-NAMESPACE Title: A-4a (REMOTE-TRUTH triage §A + ADR-0034 Decision 1 + Amendments 1 & 2): the broker effect journal's dedup key gains a minter dimension so ops minted by independent counters can never collide. ROOT (high, ground-truthed vs HEAD): the journaled-op producers key into ONE journal namespace (NET_EFFECT_SESSION|shell_sid, op) at broker.rs (EffectKey=(u64,u64)); a CLI wake op colliding with an already-journaled daemon op reproduces the typed 'already applied … retry with a fresh op_id' with NO broker restart (field-hit: spt endpoint wake id@node WOKE_FAIL). Same latent class: nethost dial_ops/stream_ops HashMap ('Shares the one net op-id namespace') would re-clobber even after the journal separates them; AND shellchan::deliver_stdin_pending journals (shell_sid, row_id) so an rc operator's ops on the same shell_sid collide with spool row ids (dropped keystroke OR dropped spool row). Amendment 2 corrected the minter set: the REAL journal minters are {cli, pump, rc, shell} + legacy — psyche/epoch are the EpochSource notif/lease counter domain, NEVER submit to apply_once, DROPPED from the journal enum (a tag with no stamp site = doc'd-but-dead knob). FIX (Decision 1 + Amdt 2): ONE canonical Minter enum {Legacy, Cli, Pump, Rc, Shell, Wake} — Legacy reserved for pre-upgrade lines + untagged wire, monotonically shrinks; enum is the single source for the TEXTUAL journal-line token (self-describing during recovery). EffectKey becomes (effect-class, minter, op); recover() DUAL-PARSES (old shorter line → minter=Legacy, new longer line → parsed tag) so old journals need no migration and old-shape keys can never equal new-shape (migration-free). A MintedOp{minter, seq} newtype REPLACES bare op_id:u64 through the brain/daemon THREADING paths so forgot-to-stamp is UNCOMPILABLE (row_id stays the shell seq — never re-minted, the durable spool exactly-once identity). Wire keeps an additive optional minter field (serde default absent ⇒ Legacy materialized at broker decode; serde_json no deny_unknown_fields ⇒ NO wire version bump); the newtype is NOT forced into wire structs. nethost op-maps re-key by (minter, op). Red-first: mint an rc op == a journaled shell/pump/daemon op int on the same session → pre-fix the second dedups/clobbers (WOKE_FAIL class); post-fix both are distinct keys, both Applied. Doc: Executor recon verified the actual journaled-op surface against HEAD; the Decision-1 tag list was wrong in both directions. Corrections, binding: PAIR 89: REQ-BOUNDARY-ROTATION-CREDENTIAL <-> REQ-SUBNET-7 ================================================================================ A) REQ-BOUNDARY-ROTATION-CREDENTIAL Title: api boundary's rotation credential is designed, documented, and eventually anchor-proven (ADR-0032): the proof is the DEPARTED session's (prior sid / token) — --to-session-id is payload, never proof; the published surface documents the adapter prior-sid persistence pattern + loud-refusal requirement; the design-true end-state additionally accepts an OS-verified parent_pid-anchor ancestry proof making adapter sid-state optional Doc: `api boundary --to-session-id --session-id ` / Boundary rotation credential: the departed-session catch-22 and the parent_pid anchor B) REQ-SUBNET-7 Title: 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) Doc: PAIR 90: REQ-HAZARD-PAIR-TRANSCRIPT-BIND <-> REQ-JOIN-DIAGNOSTICS ================================================================================ A) REQ-HAZARD-PAIR-TRANSCRIPT-BIND Title: 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) Doc: B) REQ-JOIN-DIAGNOSTICS Title: `spt subnet join` never fails SILENTLY (ADR-0030; the field incident showed no output at all). (a) LIVE progress during the meet (replace the one-shot "Searching…" cli.rs:6268 with periodic elapsed/deadline) so silence ≠ hang; (b) DETAILED failure on meet-exhaustion — rendezvous candidates + families attempted (IPv4/IPv6) + relay-vs-direct + the last concrete error — surfaced BEFORE any code prompt (a dead subnet must not make the user fetch a code); connect_seed_holder (pairhost.rs:437) and dial_via_rendezvous (meet.rs:281) currently swallow per-attempt errors — thread the last error up with attempt context; (c) PROPAGATE the terminal event — brain.rs:1024 `_ => continue` must deliver a daemon NoSeedHolder/PairFail to the CLI as a printed error (this is WHY the user saw nothing); (d) `--verbose`/`SPT_LOG` discovery TRACE (per-probe derived id, discovery path mDNS/n0-DNS/relay, per-family timeouts), opt-in — no such knob exists today. (next milestone) Doc: Robust WAN subnet join: meet-before-code + per-family bind gate PAIR 91: REQ-MESH-2 <-> REQ-PAIR-NTP-LOUD-FAIL ================================================================================ A) REQ-MESH-2 Title: 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). Doc: B) REQ-PAIR-NTP-LOUD-FAIL Title: W1/D2 (JOIN-TRUTH): total NTP failure (no server on any family answered) is LOUD, not silent — a node running the ceremony on its raw skewed system clock must be visible. ROOT: current_offset_secs (ntp.rs) does `query_offset_secs().unwrap_or(0)` and eprintln's ONLY on a nonzero success, so an all-servers-unreachable refresh is indistinguishable from 'clock agrees'. Fix: log the TRANSITION into all-servers-failed once per refresh (suggested `NTP_TOTP_UNCORRECTED: all NTP servers unreachable — ceremony clock = raw system clock`) and the recovery transition back to corrected; the OFFSET_TTL already bounds refresh cadence so no per-call spam. Fallback behavior (offset 0 → system clock) is UNCHANGED — this adds observability only. Doc: PAIR 92: REQ-PAIR-NTP-MULTIHOME <-> REQ-SUBNET-ADMIN-SEED-ROTATION ================================================================================ A) REQ-PAIR-NTP-MULTIHOME Title: W1/D1 (JOIN-TRUTH): the ceremony NTP query reaches a server on EITHER IP family — `query_unix_secs` (ntp.rs) must iterate every address `to_socket_addrs()` resolves (not just the first) and bind a socket of the matching family per candidate (IPv4 addr → bind 0.0.0.0:0; IPv6 addr → bind [::]:0), first successful answer wins. ROOT (proven 3/3-FAIL via our exact code on enlyzeam): today `UdpSocket::bind(("0.0.0.0",0))` is v4-only and `send_to(&packet, server)` sends ONLY to the FIRST resolved addr — time.google.com resolves 4×AAAA before any A on a v6-first dual-stack box → the primary server is PERMANENTLY unreachable via our code (w32tm reaches it over v6), silently halving NTP redundancy (pool.ntp.org v4 carried everything; a DNS rotation making BOTH v6-first would zero it). Fix keeps the lazy-cache/TTL/fallback contract of REQ-PAIR-8 unchanged — only the socket/resolve leg changes. Doc: B) REQ-SUBNET-ADMIN-SEED-ROTATION Title: Evicting a node rotates BOTH subnet seeds, not just the member seed. ADR-0005 #10 made removal real revocation by rotating the seed a removed node still holds; the two-key model (ADR-0051) hands every member a SECOND durable secret, and `rotate_seed` does not touch it — so as of the two-key wave an evicted node keeps the admin seed forever. That is worse than residual admin authority: an admin key IS a membership key (REQ-SUBNET-ADMIN-CODE-JOIN), so the evicted node can REJOIN the subnet on its admin TOTP, and eviction becomes toothless against exactly the nodes that were trusted enough to hold elevated credentials. ADR-0051's consequences acknowledge admin-seed rotation as unassigned milestone work; this is that work. Minimum shape: the eviction path rotates both seeds and redistributes both over the same replication machinery the join path uses, with the one-deep prior-generation grace applying to the member seed as today (an admin seed has no re-provisioning surface, so its grace question is answered by the same replication, not by a reveal). Gate: doc — ADR-0051 amended with the rotation rule; impl — the eviction/rotation path covering both seeds; unit — a rotated subnet's admin seed changes, and a node holding only the pre-rotation admin seed neither verifies an admin operation nor rejoins. Doc: 2a. Surfaced only to a proven admin — at mint and at rotation (amended 2026-07-30, fast-follow grill) / 4. Eviction rotates both seeds (amended 2026-07-30, fast-follow) PAIR 93: REQ-GOSSIP-ADAPTER-PROJECTS <-> REQ-PICKER-PROJECT-HISTORY-TRUTH ================================================================================ A) REQ-GOSSIP-ADAPTER-PROJECTS Title: #4: remote endpoint details (harness + project history) are gossiped, not faked. Today from_resource_row (crates/spt/src/picker/model.rs:340) hardcodes project_history=Vec::new() for every remote row and passes adapter_profile=row.resources (the blurb masquerading as the harness), and Instance/ResourceRow (crates/spt-net/src/net/registry.rs:457) carry no adapter field and no project list. Fix: additive gossip fields N-1-safe exactly like endpoint_type — Instance.adapter (composite [:profile]) + Instance.recent_projects (bounded, newest-first, project IDs only) -> thread to ResourceRow -> from_resource_row stops faking. Pre-field remote rows render '-'. Project IDs only + REQ-PICKER-PROJECT-HISTORY-TRUTH's disambiguation. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #4. Doc: B) REQ-PICKER-PROJECT-HISTORY-TRUTH Title: #1: picker project history is derived from sessions.log cwds (newest->oldest, deduped by project_id_for_dir) UNION context-store branches, EXCLUDING owlery-internal paths (any cwd under spt_home()/owlery) everywhere a project is displayed or inferred. Fixes three stacked defects (crates/spt/src/picker/data.rs): (1a) project_history_for (data.rs:372) reads ONLY context-store p-* branches, which are empty on this box -> history []; (1b) the fallback origin project (data.rs:207) is derived from info.json.cwd = latest-boot-cwd (rewritten every rebind), not origin; (1c) psyche-host sessions bind owlery-internal cwds that pollute history. Full DIRS stay available in the model (feature #5 needs them). PROJECT REPRESENTATION RULING (operator 2026-07-03): project IDs ONLY, EVERYWHERE incl local display; on ID collision disambiguate minimally via a PURE disambiguate_project_ids(entries)->display-names fn (append one-level-up parent folder and/or root drive letter, e.g. 'spt-core (projects)' vs 'spt-core (D:)'). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #1. Doc: PAIR 94: REQ-PICKER-3 <-> REQ-PICKER-4 ================================================================================ A) REQ-PICKER-3 Title: 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) Doc: B) REQ-PICKER-4 Title: 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) Doc: PAIR 95: REQ-PICKER-ADAPTER-DESCRIPTION <-> REQ-XTASK-SPT-BIN-TARGET-DIR ================================================================================ A) REQ-PICKER-ADAPTER-DESCRIPTION Title: 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) Doc: B) REQ-XTASK-SPT-BIN-TARGET-DIR Title: #13 (F026 micro, tooling): xtask `spt_bin()` (crates/xtask/src/main.rs) BUILDS `spt` via cargo (which honors CARGO_TARGET_DIR) but returns a HARDCODED `/target/debug/spt` path — so under a redirected target dir (CI / isolated-gate rigs that set CARGO_TARGET_DIR to a throwaway) the binary lands in `$CARGO_TARGET_DIR/debug` while xtask looks in `/target/debug` -> NotFound -> `xtask check` (docs-drift gate) spuriously fails. Workaround was running `xtask check` with CARGO_TARGET_DIR unset. FIX: a pure `target_debug_dir(root, CARGO_TARGET_DIR)` seam mirroring cargo's resolution — absolute override as-is, relative resolved against `root` (the dir cargo is invoked in), default `/target` — join `debug`; `spt_bin` returns from it. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md. Doc: PAIR 96: REQ-PICKER-FORK-LABEL-CWD <-> REQ-PICKER-UX-V013 ================================================================================ A) REQ-PICKER-FORK-LABEL-CWD Title: B-3 (F029, operator): the confirm-panel `Fork endpoint` option label is static and says nothing about WHERE the fork lands. A fork runs in the picker's launch cwd (run_cwd); the label must state that dir honestly: `Fork endpoint here --> `. Anchor picker/view.rs confirm_option_label (was `fn(opt)->&'static str`). Make the label model-aware for the dir-relative options. See triage B-3. Doc: B) REQ-PICKER-UX-V013 Title: `spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0) Doc: PAIR 97: REQ-PICKER-PROJECT-HISTORY-TRUTH <-> REQ-PICKER-SHORTCUT-LABEL-FILENAME ================================================================================ A) REQ-PICKER-PROJECT-HISTORY-TRUTH Title: #1: picker project history is derived from sessions.log cwds (newest->oldest, deduped by project_id_for_dir) UNION context-store branches, EXCLUDING owlery-internal paths (any cwd under spt_home()/owlery) everywhere a project is displayed or inferred. Fixes three stacked defects (crates/spt/src/picker/data.rs): (1a) project_history_for (data.rs:372) reads ONLY context-store p-* branches, which are empty on this box -> history []; (1b) the fallback origin project (data.rs:207) is derived from info.json.cwd = latest-boot-cwd (rewritten every rebind), not origin; (1c) psyche-host sessions bind owlery-internal cwds that pollute history. Full DIRS stay available in the model (feature #5 needs them). PROJECT REPRESENTATION RULING (operator 2026-07-03): project IDs ONLY, EVERYWHERE incl local display; on ID collision disambiguate minimally via a PURE disambiguate_project_ids(entries)->display-names fn (append one-level-up parent folder and/or root drive letter, e.g. 'spt-core (projects)' vs 'spt-core (D:)'). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #1. Doc: B) REQ-PICKER-SHORTCUT-LABEL-FILENAME Title: B-4 (F029, operator): the confirm-panel shortcut option label is a static `New/Update spt- shortcut (s)` placeholder — it should name the REAL file it writes: `Set shortcut here --> /` where is the EXACT on-disk filename (incl. extension). The name must be produced by the SAME function that names the file in shortcut creation (picker/shortcut.rs shortcut_filename over the manifest-resolved basename) so label and writer can NEVER drift. Anchor picker/view.rs confirm_option_label. See triage B-4. Doc: PAIR 98: REQ-HAZARD-SELF-ELEVATE <-> REQ-SUBNET-6 ================================================================================ A) REQ-HAZARD-SELF-ELEVATE Title: 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. Doc: B) REQ-SUBNET-6 Title: 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) Doc: PAIR 99: REQ-HAZARD-PERCH-RECORD-POWER-LOSS <-> REQ-SELF-DETECT-PARENT-PID ================================================================================ A) REQ-HAZARD-PERCH-RECORD-POWER-LOSS Title: Authoritative/identity records fsync data before the rename (5.13): a hard reset must not resurrect a full-length NUL-filled record — SCOPED, not a blanket fsync Doc: 5.13 Atomic write leaves data un-synced before the rename → NUL zero-fill on power loss `[REQ-HAZARD-PERCH-RECORD-POWER-LOSS]` Failure:** `atomic_write_bytes` was `fs::write(tmp)` + `rename(tmp, path)` with no `fsync`. The rename's directory **metadata** is journaled durable, but the tmp file's **data blocks** are still in the page cache. A hard reset (power loss, forced reboot) between the two flushes lands the rename but loses the data → the file reappears at its **full length filled with NUL**. Field incident: after a machine restart `owlery/hall-a/info.json` was 360 bytes of all-NUL (the n B) REQ-SELF-DETECT-PARENT-PID Title: E-1 (REMOTE-TRUTH triage §E-1 #7): self-detect leg (c) — the pid-ancestry fallback — ALSO candidates on `rec.parent_pid` (the harness pid, CONTEXT's 'stable session-binding anchor', stamped at bind), not `rec.pid` alone. ROOT: for an spt-hosted endpoint (broker PTY, headless) `rec.pid` is the ephemeral bind-CLI pid, ALREADY DEAD by send time (the F-026 #11 dead-pid class, field-sighted on hall-bf) — never in any sender's ancestry and alive-gated out — so an spt-hosted sender could NEVER resolve self via leg (c): its messages were from-stamped `cli@NODE` (operator #7) and replies bounced NO_PERCH. FIX: detect_self_by_ancestry pushes a second candidate (id, parent_pid) when `rec.parent_pid` is Some + alive; the pure nearest-first matcher (match_self_by_ancestry) is unchanged. LABEL-ONLY, exactly like the rest of leg (c): from-label/routing default, NEVER authentication — authenticate() untouched, the pid-ancestry-for-auth question stays parked (KH 7.3/7.5 separation holds; a wrong label self-corrects, a wrong grant does not). Env legs (a)/(b) stay first. Red-first int (the triage-specified missing test): rec.pid = dead sibling + rec.parent_pid = genuine live ancestor → self resolves (pre-fix None); ancestry-gate control: live-but-non-ancestor parent_pid must NOT resolve. Rider (same cluster, activated separately once doyle rules the fix shape): F-026 #11 dead-pid itself — rec.pid should hold something that stays true, or liveness readers stop trusting it. Cross-node from-stamp proof (spt-hosted B-side sender arrives at A as `@node`, not `cli@node`) rides the [twohost] rig wave rung. Doc: PAIR 100: REQ-BIND-HONEST-SELF-STAMP <-> REQ-HAZARD-LIVEHOST-BOOT-RACE ================================================================================ A) REQ-BIND-HONEST-SELF-STAMP Title: C2-ROOT (F028, doyle RCA 2026-07-03): the identity-attribution ROOT behind REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION — three composing defects let a psyche-hosted SessionStart hook stamp a FOREIGN perch: (1) roster::detect_self_id leg (a) scans the owlery ONE level (roster.rs:107 read_dir(owlery)) so a NESTED psyche perch owlery//nested/-psyche can never self-resolve; (2) [session.psyche_init] (claude-spt manifest ~L347) spawns the psyche with NO env_remove + no pinned child identity, so whatever OWL_SESSION_ID/SPT_AGENT_ID reaches the child resolves to a foreign top-level perch; (3) the hook then writes info.json (session_id/pid rebind) + sessions.log on the mis-resolved victim. FIX (spt-core half): (i) identity PINNED at spawn — ManifestRuntime role spawns inject the child's OWN SPT_AGENT_ID= + OWL_SESSION_ID=, AND detect_self_id enumerates NESTED perches (fix the one-level owlery scan); (ii) BIND HONESTY — a session-start stamp may only write a perch whose resolved id AFFIRMATIVELY matches; never a fallback pick; refuse + loud-skip when unresolved (kin REQ-MSG-CLI-ORIGIN honest-default + #9 ancestry). ADAPTER half = PERRI touchpoint (psyche_init env scrubbing / relies on runtime pinning; hook loses silent fallback-perch behavior) — FLAG doyle BEFORE any manifest move (adapter glue-model rule). See triage C2 fix (i)+(ii). Doc: B) REQ-HAZARD-LIVEHOST-BOOT-RACE Title: 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. Doc: PAIR 101: REQ-CI-WINDOWS-PHASE-A-BOUND <-> REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT ================================================================================ A) REQ-CI-WINDOWS-PHASE-A-BOUND Title: Phase-A nextest parallelism is BOUNDED on the Windows self-hosted leg, so the full-parallel 1881-test battery stops saturating a box that also hosts the live agent fleet. (Load-flake family leg 2 = deployah option (c), doyle-ratified 2026-07-22.) Windows leg ONLY: kitsubito does not host the fleet, and its parallelism is deliberately untouched. MECHANISM AND WHY THIS ONE: the bound lives in a [profile.ci-windows] nextest profile selected by a STEP-LEVEL NEXTEST_PROFILE expression on the Phase-A step, NOT by a per-OS pair of Phase-A steps — duplicating that step would put a FOURTH copy of the filter string in the tree, and .config/nextest.toml's own header names keeping THREE in sync as the standing hazard. THE NON-INHERITANCE GATE-CHECK (doyle demanded it discharged, and it did not come back clean): a custom nextest profile inherits scalar settings from default but NOT profile.default.overrides — the argument cuts both ways. Of the five overrides on default, exactly ONE is kind(test): the by-binary integration filter, which is Phase B's alone and needs no mirror. The other FOUR are the FLAKE-LEDGER #14 class — real-broker units that are kind(bin)/kind(lib), escape the by-binary filter, and therefore RUN IN PHASE A depending on the heavy-broker-pty group to stay serialized (rc::tests four names, (applyhost|livehost|pump)::tests, wansend::tests, api::startup::tests). Shipping the profile without them would have SILENTLY UN-SERIALIZED ON WINDOWS the exact class whose un-serialization produced two separate 240s timeouts. They are mirrored verbatim, and the parity is ENFORCED rather than remembered: xtask check's check_phase_a_profile_parity fails the build when a default override that is not kind(test) is missing from ci-windows, keyed on that SHAPE and not on a name list — the same posture as FLAKE-LEDGER #14's second half, whose whole lesson is that a class written in prose and shipped as an enumeration decays. A DRIFTED mirror reads as missing, not as present: the invariant is that it is the SAME filter in both profiles, not that something is in both. THE BOUND ITSELF IS CHOSEN, NOT DERIVED, and is recorded that way at the value: hfenduleam is 16 logical cores and the bound is 8 (half), for fleet-co-tenant headroom. No measurement separates 8 from 6 or 12 and none was run — the ratified fix is stop saturating the box, not find the saturation knee. Retuning is one integer. Gate: impl — the ci-windows profile with its mirrored overrides plus the ci.yml step-level profile selection plus the xtask parity check; unit — the parity predicate's exemption rule (a kind(test) override needs no mirror), its detection of an unmirrored Phase-A override, its rejection of a DRIFTED mirror, and a case over the REAL checked-in config so the check cannot ship green against a tree that already violates it. Kin REQ-CI-POSTJOB-DAEMON-REAP (the other cause-side leg), REQ-HEAVY-UNIT-CLASSIFICATION (the check one layer in), FLAKE-LEDGER #14/#15. Doc: B) REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT Title: 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) Doc: PAIR 102: REQ-DIGEST-GENERATION-SUPERSEDE <-> REQ-HAZARD-LIVEHOST-BOOT-RACE ================================================================================ A) REQ-DIGEST-GENERATION-SUPERSEDE Title: W3 (LIFECYCLE-TRUTH, digest projection truth — flynn filing spt-mobile d0aa3f4): a one-shot `endpoint digest --json` snapshot must return each logical activity row ONCE across a checkpoint/resume, not once per seq-generation. ROOT (spt-core-side, not a consumer bug): the K-session span (digest.rs activity_spanned, SPAN_SESSIONS=5) runs the [digest] extractor per session file and tags each row seq=(ledger_ordinal<<32)|localseq (REQ-DIGEST-CURSOR). A checkpoint/resume (self-/clear + Psyche rebuild) makes the harness REPLAY the prior generation's transcript into the NEW session file, so the ancestor's rows appear in BOTH the ancestor file AND the resume file at the SAME localseq — the span UNIONS them, one logical row surfacing under two full seqs (gen23,local208) + (gen25,local208), identical text/ts/localseq. Consumers dedup by exact seq (the documented authoritative key) so nothing collapses -> duplicate rows in every snapshot / `--after` view (`--follow from:0` is CLEAN — it reads current-generation only; the SPAN is the sole culprit). The trigger cannot disambiguate: `api boundary clear` records SessionTrigger::Clear for BOTH a fresh /clear (disjoint) and a carry-forward checkpoint (reporting.rs:94) — so a structural skip-ancestor needs new boundary metadata + adapter cooperation, deferred. FIX (doyle ruling — flynn Option 1 Supersede, projection-local, source-independent): within the span, collapse cross-generation replay dupes — an Activity record from an OLDER ordinal is dropped when an identical logical record (role, ts, text, tool) exists under a NEWER ordinal; keep the NEWEST-ordinal occurrence so the surviving seq is the live generation (snapshot + follow agree on seq). Cross-generation ONLY (never dedup within one ordinal — a session cannot replay itself; identical within-gen rows are real). Supersede runs on the raw span items BEFORE the window fold (project_timeline) so window_turns counts real turns, not phantoms; and a boundary divider adjacent to a now-fully-superseded ancestor is not left orphaned. Context entries (REQ-TERM-7, single digest.log) are not per-session-spanned -> untouched. Int = a two-session span rig where session B's extracted lines are a superset replay of A (same ts/text at same localseq) + B's own new tail: RED-first (pre-fix shows every A row twice); post-fix each logical row appears ONCE under B's generation, B's tail intact, the /clear boundary marker preserved when A retains rows. Doc: B) REQ-HAZARD-LIVEHOST-BOOT-RACE Title: 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. Doc: PAIR 103: REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE <-> REQ-HEAVY-UNIT-CLASSIFICATION ================================================================================ A) REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE Title: 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) Doc: B) REQ-HEAVY-UNIT-CLASSIFICATION Title: A unit test that stands up a REAL broker inside a lib/bin `#[cfg(test)]` block must sit in the `heavy-broker-pty` nextest group, and the classification must be ENFORCED rather than remembered. FLAKE-LEDGER #14 diagnosed this class in the `spt` binary, wrote the CLASS in prose, then shipped an ENUMERATION of four `rc::tests::` names — so the identical shape in `spt-daemon`'s lib (`applyhost`: a real `Broker::bind` + `serve()` in 10 of its 13 units) stayed in the full-parallel Phase-A pool and TIMED OUT at 240s twice, at v0.32.0 and again under the v0.39.0 W5 gate, the v0.32.0 remedy never having landed. The defect is provable from `.config/nextest.toml` ALONE (two overrides, neither matching `kind(lib)`); timing evidence only ever estimated the rate. Gate: impl — heavy-group overrides for `applyhost`/`livehost`/`pump` (spt-daemon lib) and `wansend` (spt bin, found BY the check rather than by a person), plus `xtask check`'s `check_heavy_unit_classification` keyed on the SHAPE (a `Broker::bind` after the `mod tests` marker) instead of a name list; unit — the two pure seams, including the regression for this check's OWN first draft, which substring-matched `::tests` and so missed every module written inside an alternation group. Kin FLAKE-LEDGER #14/#15, REQ-CI-DOCS-ONLY-THIN. Doc: PAIR 104: REQ-HAZARD-DAEMON-STOP-REAP <-> REQ-PICKER-WINDOW-TITLE ================================================================================ A) REQ-HAZARD-DAEMON-STOP-REAP Title: 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) Doc: B) REQ-PICKER-WINDOW-TITLE Title: B-5 (F029, operator): `spt endpoint run`'s interactive picker window/tab is untitled — hard to find among many terminals. Set the window/tab title to `SPT Endpoint Picker`. Anchor picker/mod.rs:88 setup_terminal (crossterm SetTitle in the execute! chain). Set-only is acceptable (crossterm can't cheaply read the prior title to restore). Applies ONLY to the interactive picker path — non-interactive/headless `endpoint run` (REQ-HOST-RUN-1) must NOT retitle the operator's terminal. See triage B-5. Doc: PAIR 105: REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE <-> REQ-SESSION-ADAPTER-RECORDED ================================================================================ A) REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE Title: 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) Doc: B) REQ-SESSION-ADAPTER-RECORDED Title: D-2 (REMOTE-TRUTH triage §D-2 + operator Q5 @c248afc): the session ledger records the adapter[:profile] a session ran under, so a later resume can restore the harness the session actually used (not merely the endpoint's CURRENT stamp). ROOT: SessionEntry (spt-store/sessions.rs:58) carries ts/session_id/trigger/cwd/ordinal but NOT the adapter — a resume-from-history row cannot know which harness authored the transcript, so a resume under a since-changed endpoint adapter (B-2 ChangeAdapter, or a fork) launches the wrong harness. FIX: an ADDITIVE `adapter: Option` on SessionEntry, exact cwd/ordinal serde pattern (#[serde(default, skip_serializing_if="Option::is_none")]) — a pre-migration row missing the key deserializes None; None omits the key on serialize (byte-identical to old rows); an unknown key on an old reader is ignored (serde default) — back-compat BOTH directions. Stamped at every PRODUCTION session-boundary append. CENSUS (doyle-confirmed @94f0205, corrects the triage-era 5-site drift to the real 3): startup.rs:317 (live bind boot row, rec.adapter in scope), reporting.rs:94 (boundary rotation row UNDER the mutate_info lock, capture adapter_for_ledger=rec.adapter beside cwd_for_ledger), ready.rs:119 in crates/spt-msg (ready-agent boot row, rec.adapter in scope). NOT digest.rs:601 (cfg(test) fixture) and NOT a livehost psyche-ledger append (none exists — the live /clear|/compact boundary shells `api boundary` → reporting.rs:94, the SAME append). None-stamp is a benign degrade (resume falls back to the endpoint's current adapter). Doc: PAIR 106: REQ-HAZARD-PSYCHE-OUTBOUND-PROXY <-> REQ-INST-2 ================================================================================ A) REQ-HAZARD-PSYCHE-OUTBOUND-PROXY Title: 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) Doc: B) REQ-INST-2 Title: Per-node files, synced Psyche mind Doc: PAIR 107: REQ-HAZARD-TEMPLATE-ARGV-FILL <-> REQ-SPOOL-TAKE-AUDIT ================================================================================ A) REQ-HAZARD-TEMPLATE-ARGV-FILL Title: 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. Doc: B) REQ-SPOOL-TAKE-AUDIT Title: W5 (LIFECYCLE-TRUTH, RCA cost: proving WHO took delivered=1 rows burned an hour): the spool records the taker per row — leg enum (relay-backlog / hook-poll / idle-inject / psyche) + sid/pid + taken_at ms — surfaced by a --json debug read. Additive column, no schema break (delivered rows already retained). Doc: PAIR 108: REQ-HAZARD-WORKER-PATH <-> REQ-PSYCHE-TURN-STREAM-EVIDENCE ================================================================================ A) REQ-HAZARD-WORKER-PATH Title: Single source of truth for Worker/Psyche perch location (1.5) Doc: B) REQ-PSYCHE-TURN-STREAM-EVIDENCE Title: A failed psyche turn preserves BOTH captured streams as evidence: TurnError::Failed carries the child's stdout alongside stderr, and the failure display appends a bounded single-line stdout TAIL (last ~500 bytes, UTF-8-boundary-safe cut, newlines collapsed, the literal when the stream said nothing — absence stated, never implied by a missing field). WHY (2026-07-26 spend-limit RCA): a failed turn's stdout is not psyche output, so BOTH core (turn.rs kept {status_code, stderr} only) and the adapter (guarding its outbound channel) independently discarded it — the same blind spot implemented twice — and an account-level outage surfaced as a bare 'claude exited exit code: 1' with the decisive refusal text thrown away at two layers; three agents then chain-hypothesized on an error string the real failing path never emitted. The tail is cause-agnostic instrumentation: it does not care what the failure is, which is why it survives being wrong about it. Adapter twin: claude-spt v0.25.14 dual-stream tail (shipped 2026-07-26). Doc: PAIR 109: REQ-HOST-RUN-2 <-> REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION ================================================================================ A) REQ-HOST-RUN-2 Title: 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). Doc: B) REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION Title: C2 (F028, infra; ROOT-CAUSED + severity-upgraded doyle RCA 2026-07-03): cross-endpoint perch contamination — a foreign psyche's SessionStart hook REBINDS a victim perch's IDENTITY, not merely its ledger. Evidence: hall-a's info.json.session_id IS f015b-probe-psyche's session (359d7bd7) + hall-a's ledger holds the foreign psyche session; same class as hall-b's dead-pid stamp (141556). This REQ = the OBSERVABLE (foreign session_id in a perch's ledger/info.json + resume offering foreign sessions) and its belt-braces: (iii) filter owlery-cwd rows OUT of resume_rows; (iv) one-time repair for already-contaminated perches (hall-a on HFENDULEAM) or self-heal on next legitimate session-start. The ROOT (identity pinned at spawn + honest bind + nested self-resolve) is REQ-BIND-HONEST-SELF-STAMP — C2 is UPSTREAM of B3 (presence/CONTROLLED read the very stamps this corrupts). See triage C2. Doc: PAIR 110: REQ-LIVENESS-ORACLE-SOUND <-> REQ-PSYCHE-CONTEXT-FILE-INDIRECTION ================================================================================ A) REQ-LIVENESS-ORACLE-SOUND Title: TEARDOWN-AUTHORITY W2 (todlando W1 gate-round-0 finding, doyle-scoped from the LANDED W1 code 2026-07-19): 'does this pid still exist' has ONE answer in spt-core and it is derived from the OS process table. TODAY spt-daemon/src/broker.rs session_is_zombie computes wrapper_alive from spt_store::proc::is_process_alive, which probes OpenProcess on Windows — and OpenProcess keeps SUCCEEDING for a TERMINATED process while any parent holds an open handle, which the broker ALWAYS does (Arc) for every PTY child it spawned. A correctly-reaped harness therefore reads ALIVE, flipping zombie_verdict off its PRIMARY class (Some(false) = dead root + surviving record = always a zombie) onto the conditional arm, which additionally demands adapter_labeled && past_grace && !has_live_descendants. CONSEQUENCE, live today: a dead-root session that is NOT adapter-labeled is claimed LIVE indefinitely — `endpoint run`'s dup-guard refuses ENDPOINT_ALREADY_LIVE over an already-dead tree and cmd_rest's Suspend alive_hint forces from=alive on the same false claim (both via has_live_session_honest, cli.rs:2014 and :3805). REQ-ENDPOINT-CYCLE-HONEST exists to give the cycle verbs ONE liveness authority; after W1 there are TWO and they disagree by construction (teardown.rs::root_provably_gone asks the table and is right; session_is_zombie asks is_process_alive and is wrong). THE DISCRIMINATOR (binding, and it makes the audit checkable rather than 20 judgement calls): is_process_alive is unsound EXACTLY when the ASKER — or a live ancestor — still holds an open HANDLE to the target; dropping a Child closes it, so a spawner that DROPS is honest and one that RETAINS is not. Hence the dangerous shape is asking 'is it GONE' about a process you OWN, and the SAME CALL IS SOUND IN THE CLI AND UNSOUND IN THE DAEMON FOR THE SAME PID — soundness is a property of the asker, not the call. REJECTED ALTERNATIVE, recorded in ADR-0045 Amendment 1 so it is not re-proposed: proc::reap_if_child before the probe fails twice — it is a NO-OP on Windows (#[cfg(windows)] let _ = pid), and decisively the broker holds the handle BY CONSTRUCTION for every pid this predicate is ever asked about, so no handle-based probe can EVER be sound at this site. FIX: (a) add a sound probe as a NAMED SIBLING whose name is the question — process_exists(pid) over process_table() — and state on is_process_alive's own doc which question it answers and which it does not, pointing at the sibling (the behavior was documented VERBATIM in legacy_resident_sweep_e2e.rs since 2026 and never reached proc.rs or zombie_verdict: written where DISCOVERED, not where CONSUMED); (b) route session_is_zombie's wrapper_alive through it, leaving zombie_verdict PURE and UNCHANGED (it was fed a lie, it is not wrong); (c) an EMPTY table is NO KNOWLEDGE — it must resolve to None (zombie_verdict(None) already means 'never guess') and must NEVER manufacture Some(false), which would mass-classify every live session a zombie = the W1 blocker inverted at broker scope; (d) AUDIT, do not mass-migrate, the remaining callers using the discriminator — roster.rs/api/startup.rs/api/auth.rs are already adjudicated SOUND (the asker never spawned the target); cli.rs purge psyche-quiesce is RIGHT ANSWER FOR A FRAGILE REASON (sound only because the CLI asks and the DAEMON spawned the psyche — it goes unsound SILENTLY if that check ever moves into the daemon) and gets a comment naming the asker as what makes it safe; the daemon-side population (livehost.rs, shellhost.rs, shellwake.rs, lifecycle.rs, in-daemon is_perch_alive) is adjudicated one verdict per site (sound/unsound/unreachable), fixing only unsound AND reachable and REPORTING the verdicts even where left — if unsound-and-reachable exceeds a handful, STOP and escalate to doyle rather than widening this wave. Gate: doc — KNOWN-HAZARDS 7.50 + ADR-0045 Amendment 1; impl — process_exists + the caveat on is_process_alive + wrapper_alive routed + the adjudicated fixes; unit — the empty-table arm resolves to None (never Some(false)), a handle-held corpse classifies as a zombie, and zombie_verdict's existing pure table is extended with the previously-unreachable case (dead root + not adapter-labeled + within grace); int — manufacture the REAL handle-held-corpse condition (broker retains a handle to a killed PTY child) and assert the session classifies zombie + the run dup-guard does NOT refuse over it; Windows-only if it cannot be made on Linux, and the test NAME says so. Doc: Failure (paid-for, found by todlando during TEARDOWN-AUTHORITY W1 gate round 0, 2026-07-19; latent in `session_is_zombie` since the cycle verbs were built):** `spt_store::proc::is_process_alive` probes `OpenProcess` on Windows, which keeps SUCCEEDING for a TERMINATED process while any parent still holds an open handle to it — and the broker holds `Arc`, hence such a handle, for every PTY child it spawned. So a correctly-reaped harness reads ALIVE. `broker.rs::session_is_zombie` feeds exactly that call into `zombie_verdict`'s `wrapper_alive`, which flips the verdict off its PRIMARY B) REQ-PSYCHE-CONTEXT-FILE-INDIRECTION Title: W4 (F030; doyle Q2 ruling + perri file-always freeze, 2026-07-04): the composed psyche mind ({psyche_context}) rides the SHIM argv today — a real ~20KB doyle psyche-download exceeds the win32 CreateProcess lpCommandLine ~32k cap → the shim spawn BRICKS. FIX (file-always, replaces {psyche_context} outright — no size-branch, no argv cliff, one path): core writes the mind to a file in the psyche's NESTED perch dir BEFORE each turn spawn and fills a single {psyche_context_file} = that PATH (argv-cap-immune). The soft fresh/continue discriminator moves from KEY-presence to FILE-CONTENT: FreshWithPreload writes the composed mind NON-EMPTY (the never-empty guarantee carries to the file content); ContinueExisting writes it TRULY 0-BYTE (perri BINDING PIN 1 — NO sentinel/placeholder EVER, else her non-empty=fresh discriminator misfires a spurious --session-id adopt). Core owns the file lifecycle: write-before-spawn each turn, overwrite in place, persists between turns in the nested perch (debuggability); never deleted per turn. perri shim delta: --psyche-context-file arg, read-file prefix, TRIM-based emptiness (her tolerance, NOT core's license — core writes exactly 0 bytes on continue, PIN 2), read-failure = generic fail NEVER 95, never writes/deletes the file. Red-first: a ~40KB mind → the old {psyche_context}-on-argv path BRICKS the win32 shim spawn; the file path succeeds (shim reads the full mind from file). Doc: Psyche-download — `{psyche_context_file}` (file-always, replaces `{psyche_context}`).** The composed Psyche mind rides a **file**, never the command argv: before each turn spt-core writes the mind into the nested psyche perch dir and fills a single **`{psyche_context_file}` = that path** (argv-cap-immune — a real ~20 KB mind exceeds the win32 command-line cap and would brick the spawn). The soft **fresh-vs-continue** discriminator is the file's **content**, not key presence: a **fresh** (first / reseeded) turn writes the composed mind **non-empty** (a never-empty `` marker when PAIR 111: REQ-PICKER-WINDOW-TITLE <-> REQ-PSYCHE-EPHEMERAL-DRIVER ================================================================================ A) REQ-PICKER-WINDOW-TITLE Title: B-5 (F029, operator): `spt endpoint run`'s interactive picker window/tab is untitled — hard to find among many terminals. Set the window/tab title to `SPT Endpoint Picker`. Anchor picker/mod.rs:88 setup_terminal (crossterm SetTitle in the execute! chain). Set-only is acceptable (crossterm can't cheaply read the prior title to restore). Applies ONLY to the interactive picker path — non-interactive/headless `endpoint run` (REQ-HOST-RUN-1) must NOT retitle the operator's terminal. See triage B-5. Doc: B) REQ-PSYCHE-EPHEMERAL-DRIVER Title: W1 (F030, design §3): each psyche-relevant event runs exactly ONE bounded per-event turn through the existing driver stack (psyche_turn_and_relay for outbound-intent events / resume_psyche for session-custody transitions / run_psyche_turn for pure merges) — no resident psyche process exists between events. host_one (livehost.rs:518) STOPS spawning spawn_psyche_owned; the pulse loop stays as the daemon-side scheduler (thread + stop-flag + drop-dir watch correct) but a fire now invokes one bounded turn, daemon-driving every substitution key from daemon-known context (child never self-resolves home/subnet/perch — direction-(a) multi-subnet churn impossible by construction). Turn failures consume a bounded failure budget (C3(b) shape): N consecutive failures → psyche_host_error stamp + cooldown, reset on success; no respawn storm (nothing resident to respawn). Red-first: fire an event on a hosted live endpoint → assert one turn ran (SIDE-EFFECT PROOF FILE — transcript-jsonl asserts are structurally blind, 2026-07-04 rig lesson) and no {id}-psyche process survives the turn. Doc: Psyche**: The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. **Liveness = turns succeed** — never PAIR 112: REQ-PSYCHE-LEGACY-RESIDENT-SWEEP <-> REQ-PSYCHE-SID-CUSTODY ================================================================================ A) REQ-PSYCHE-LEGACY-RESIDENT-SWEEP Title: W5 (F030; doyle+perri 2026-07-04): a dirty daemon upgrade from <=v0.24.0 strands a RESIDENT psyche wrapper the OLD daemon spawned — and F-030 W4's nested-`ready` resolution fix CONVERTED that wrapper's accidental self-reap into a permanent HANG. The pre-W3 wrapper's only spt IPC is `spt ready -psyche --once` (BLOCKING, no internal timeout); pre-W4 that hit READY_FAIL on a multi-subnet home → the wrapper exit-4'd (accidental reap). Post-W4 the nested id resolves cleanly → the wrapper REGISTERS then BLOCKS FOREVER on its first post-upgrade poll: no exit, no psyche_host_error, no CPU (KH 2.6 invisible-loop class, one level up). Post-W3 core has no residency machinery to reap it. FIX: a ONE-SHOT legacy-resident sweep at BRAIN START (never per-reconcile/periodic — burying residency-era machinery, not resurrecting it). GUARD = adapter-AGNOSTIC (glue-model): resurrect the retired reap_orphan_psyches LOGIC — for each self-perch live-agent id derive `-psyche` and kill iff (a) exe basename == the adapter's MANIFEST-declared psyche program (normalize_basename, never a hardcoded adapter name) AND (b) cmdline contains the id marker `-psyche` AND (c) pid alive; any unreadable signal → DECLINE + loud log (fail-safe-decline, positive-match-only; infra never-kill inside the sweep). FRATRICIDE is closed by TIMING (perri-confirmed from the owning side): the ephemeral shim is daemon-spawned per-event, bounded, exits at turn end — at brain start BEFORE the first reconcile/pulse no current shim is resident, so any `-psyche` psyche-program process alive then is unambiguously stranded-legacy. RESIDUE (doyle PIN 3): the hung wrapper REGISTERED a `-psyche` ready perch before blocking; killing the pid alone leaves a phantom ready-record with a dead pid (the REMOTE-TRUTH presence-lie class) — the sweep MUST also clear that stale registration or prove the existing stale-perch cleanup reaps it. No field window pre-W6 (nothing releases). (F-030 W5) Doc: B) REQ-PSYCHE-SID-CUSTODY Title: W2 (F030, design §3): the psyche mints and keeps its OWN session id, stored in the nested {id}-psyche perch record — {session_id} in psyche role templates becomes the psyche's sid, never the parent's (today's fill at livehost.rs:518 is the PARENT's — the custody bug). Parent boundary (/clear, /compact) does NOT rotate the psyche sid (the psyche's conversational thread survives parent resets — its job). resume_psyche validates the custody key before spawn (resume.rs:183). Reseed path: psyche session lost/invalid → ResumeMode::FreshWithPreload (download_psyche_context composes role/live/project into {psyche_context}, resume.rs:100) + LOUD PSYCHE_RESEED:{id} marker (custody-loss loop visible; W1 budget bounds it). If the parent sid is still needed by a template it gets its OWN explicit key {parent_session_id} — never aliased. Red-first: parent `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's — guard-revert reproduces). Doc: Custody sid — `{session_id}` is the Psyche's OWN id.** In a psyche role template `{session_id}` is the **Psyche's own minted session id**, kept in its nested `-psyche` perch record — **not** the parent's. A parent boundary (`/clear`, `/compact`) rotates the *parent's* sid but does **NOT** rotate the psyche sid: the Psyche's conversational thread survives parent resets (that is its job). When a template still needs the parent's sid it takes the **explicit** `{parent_session_id}` key — never an alias of `{session_id}`. PAIR 113: REQ-PUBLIC-ERROR-SURFACES <-> REQ-SEAM-PSYCHE ================================================================================ A) REQ-PUBLIC-ERROR-SURFACES Title: F-1 (REMOTE-TRUTH triage §F-1, Q4 UX rule, operator-ruled): CLI stderr a non-developer can hit names the OBSERVABLE SITUATION + the NEXT ACTION — never journal/op/brain/store lingo. The sweep's named offenders: (1) `RC_FAIL:{id}: … brain IPC read deadline elapsed` — the brain transport error surfaced RAW through rc's residual Err arm (rc.rs run_attach_inner); operators read 'brain IPC' where the situation is 'the daemon didn't answer in time'. (2) `WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch` (resting.rs apply_event miss) — store-file lingo in the one rest-verb line a stale remote row still surfaces cross-node (the qualified-arm D6 case; the A-3 bare-id local path already routes instead). The miss stays SINGLE-SOURCED from NOT_A_HOSTED_PERCH_MARKER (in-process discriminant, resting.rs — reword is compat-safe per its own doc; the drift-pin unit keeps builder+matcher fused). (3) translation_fault never human-rendered (F-030 post-release seed): a broker-stamped input-translation fault (e.g. 'inject worker panicked') was invisible in `endpoint list`/`whoami` while keystrokes silently degraded — rendered now as a SELF-pin annotation exactly like the psyche_host_error pattern (REQ-HAZARD-LIVEHOST-BOOT-RACE), human line + additive skip-if-none JSON field. Kin to banked patterns: public --help no internal codes; 'Updated' not 'trial'. The A-4b retry terminal + B-3 give-up line + A-3 routing strings shipped F-1-clean already — this REQ sweeps the stragglers and is the home for future sightings (extend, don't multiply). Doc: B) REQ-SEAM-PSYCHE Title: spawn-psyche seam (fresh + resume templates) Doc: PAIR 114: REQ-ER-RC-INTENT-LOCKS <-> REQ-HAZARD-RC-ATTACH-FAILFAST ================================================================================ A) REQ-ER-RC-INTENT-LOCKS Title: The engine room's attach surface is locked by INTENT: rc --view is denied even same-node, remote attach of any intent is denied, and local --take is allowed (ADR-0052 decision 3). Viewing is denied because a read-only spectator on the surface that renders access rulesets is an information leak with no operator benefit — and because the same-node loopback allow precedes the resolution chain, this must be an explicit engine-room carve-out rather than a rule row that the loopback shortcut would skip past. Local take is allowed on two grounds, neither of them the original 'it forces a harness restart' (it does not — a take is a broker lease displacement and restarts nothing): the displacing controller must pass the same bring-up gate the incumbent passed, so a take is a gate attempt and is bounded by the same ledger (REQ-ER-BRINGUP-ATTEMPT-BOUND); and the displacement is LOUD, so an incumbent human cannot be silently unseated. Gate: doc — ADR-0052 decision 3 and the section 3a amendment; impl — the intent-keyed engine-room carve-out ahead of the chain, the remote-attach refusal, and the take path's gate pass plus empowerment revocation; unit — same-node view denied, remote attach denied for every intent, local take admitted only after a gate pass, and a take revoking empowerments. Doc: Decision 3 was written in the language of the ceremony, and two of its clauses do not survive contact with the daemon's lifecycle invariants. Both are amended here rather than built literally. B) REQ-HAZARD-RC-ATTACH-FAILFAST Title: 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) Doc: PAIR 115: REQ-ADAPTER-MULTIPLATFORM-SPT <-> REQ-DOCS-RELEASE-ASSET ================================================================================ A) REQ-ADAPTER-MULTIPLATFORM-SPT Title: A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `/` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2) Doc: Multi-platform adapter `.spt` packaging B) REQ-DOCS-RELEASE-ASSET Title: THE-FORKENING W2 (ADR-0036 §4): every release ships a platform-independent docs bundle `spt-docs.tar.gz` (BUILT mdbook output: HTML + llms.txt + llms-full.txt + raw .md + manifest.schema.json) as a release asset WITH an entry in the SIGNED update-set (sha256, same integrity chain as binaries — docs describe the security-relevant contract surface, they do not ride unverified). Apply lands/refreshes $SPT_HOME/docs (single current copy = docs always match the installed binary). FAILURE ISOLATION binding: a docs-asset failure NEVER fails the binary update — UPDATE_DOCS_SKIPPED loud, retried next fetch. Gate: unit — update-set entry + sha256 verify + skip-loud isolation; int — a fetch+apply lands version-matched docs at $SPT_HOME/docs; doc — self-update docs name the bundle. Kin REQ-DOCS-LOCAL-SERVER (the consumer), REQ-RELEASE-CHANNEL-PRIVATE (the assemble leg), ADR-0036. Doc: docs bundle** — every release ships a platform-independent archive of the **built docs** (HTML + `llms.txt` + `llms-full.txt` + raw markdown + `manifest.schema.json`) as a **signed update-set asset**; apply lands it at `$SPT_HOME/docs`, so a node's docs always match its installed version. A docs-asset failure never fails the binary update (skip loud, retry next fetch). Consumed by the *docs server* (below). PAIR 116: REQ-ADAPTER-UPDATE-MESSAGE <-> REQ-UPD-9 ================================================================================ A) REQ-ADAPTER-UPDATE-MESSAGE Title: An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's "run `/reload-plugins` in any ongoing sessions". (v0.13.2) Doc: adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025): A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `/` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. B) REQ-UPD-9 Title: `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). Doc: adapter update declaration** (manifest field): Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0 PAIR 117: REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE <-> REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK ================================================================================ A) REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE Title: SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 stall-evict matures (~15s). REQ-UPDATE-TRIAL-DRAIN-DRIVE (counter-54) makes the candidate DRIVE that reap so it promotes within the 30s window — but at a ~15s wedge-maturity hitch (frozen PTYs during the swap). A CLEAN close makes the conn 'simply absent → drained=false AT ONCE → fast promote' (broker.rs:2699-2700), ELIMINATING the hitch = truly seamless (honors the paradigm the field freeze broke). SUPERSEDES the earlier livehost-reattach framing of 'Fix Y': livehost is SPAWN-FRESH (fresh session uuid/pid per boot, nothing to re-attach — wrong site, and it never ran in the trial window); the correct site is the brain-cycle / update-restart path (a bounded graceful-drain of the outgoing brain BEFORE the kill). Non-trivial: hard-kill → bounded graceful drain; a drain that hangs must NOT wedge the swap (timeout then kill anyway, never block the update). Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (defense-in-depth reap-drive REMAINS for any conn that still black-holes — a peer/relay conn, a drain-timeout kill) + REQ-UPDATE-PROMOTE-DRAINED (the gate) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict). Int: a planned update-restart under a live-agent controller conn → the new candidate promotes WITHOUT waiting the ~15s wedge-maturity (drained reads false immediately, no STALL_EVICT), RED-first vs the current hard-kill-then-reap-drive ~15s hitch. Doc: B) REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK Title: UPDATE-WEDGE round-4 (hertz RCA, root reproduced + source-pinned on Windows 2026-07-09): EVERY write on a physical broker connection rides ONE bounded + cancelable + poison-on-failure framed-write primitive — no writer may hold the connection's serialized send gate across an UNBOUNDED OS write. ROOT: controller_writer held the SharedSend = Arc> guard ACROSS a blocking write_frame; on Windows interprocess 2.4.2 routes the send to WriteFileEx + SleepEx(INFINITE, alertable) with NO supported write timeout (set_timeout → Unsupported), so an `rc --take` controller consumer that stops reading blocks the write INDEFINITELY (~127.95 s in the field capture, released only when a brain restart tore the conns down). Logical stall_evict_controller (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) removes the controller ROLE but neither cancels the in-flight pipe write, closes the physical connection, nor invokes CancelIoEx — the detached writer keeps its SharedSend clone + live stack-owned mutex guard. Load-gated: needs a real seq>0 frame + a non-draining consumer (seq-0 boot conns + a quiescent update are clean — why v0.30.5's controlled apply passed); CTRL_WRITE_LOCKED wait_us=0 on all four capture writers exonerates the mutex convoy — the block is INSIDE the OS write, after lock acquisition. Existing deadlines can't bound it: CONTROLLER_WRITE_DEADLINE is output-driven channel-full handling (a writer blocked on its first owned frame never fills the channel); BRAIN_WRITE_DEADLINE is an age PREDICATE sampled opportunistically, not an I/O timer. FIX (accepted shape, doyle GO + 3 confirms): broker-owned Arc conn object (conn.rs BrokerConn) replacing raw Arc> — serialized write gate (bounded gate-wait) + send half (never leaves the object, so a bypass cannot compile) + idempotent poisoned state + per-in-flight op identity + platform abort seam, with an INDEPENDENT per-conn watchdog firing OUT OF BAND at an ABSOLUTE deadline stamped at write entry covering BOTH gate-wait and OS write completion (brain_write_deadline() — the existing SPT_BRAIN_WRITE_DEADLINE_MS knob; the abort NEVER relies on the write returning or on opportunistic stall-evict sampling). On deadline/partial/cancel/unknown completion: (1) poison the whole physical conn, (2) abort read+write (cfg(windows) CancelIoEx then DisconnectNamedPipe; cfg(unix) UnixStream::shutdown(Both)) so handle_conn reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer (interprocess write_exsync returns only after the completion APC — release = write return), (4) NEVER reuse the conn (a timed-out length-prefixed frame may be partially written), (5) join/finish the retired writer before reporting physical cleanup. Controller (replay + live), viewer, dispatch-reply (send_frame/send_error + inline dispatch_* replies), and nethost stream-log/presence writes ALL route through the primitive — leaving ANY raw unbounded write behind the gate preserves the failure class (grep-proven: no surviving raw send.lock()+write_frame on a physical conn path). NO new output queue (the bounded queue + isolated writer exist; the block is BELOW them); NO PIPE_NOWAIT (recorded mid-frame corruption risk). Broker-side only, no wire change, mixed-version peers wire-compatible; Unix keeps existing semantics under the same poison/retire invariant. Doc: 7.38 EVERY write on a physical broker connection is bounded + cancelable + poison-on-failure — no writer holds the send gate across an UNBOUNDED OS write `[REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]` Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_t PAIR 118: REQ-CRC-SWAP-OLD-DISPLACE <-> REQ-UPDATE-FINISH-COMMUNE-FLUSH ================================================================================ A) REQ-CRC-SWAP-OLD-DISPLACE Title: C1 (F028, infra; memory crc-swap-old-litter-brick, RCA'd ENLYZEAM + confirmed systemic): crc_swap `.old`-litter bricks every subsequent adapter update. apply_crc_swap Phase-3 `let _ = remove_file(.old)` (spt-daemon/src/crc_swap.rs:129-133) silently fails whenever ANY pre-update process still image-maps the old binary (NORMAL on a live box — endpoint launch children survive updates). The NEXT update's first commit-op rename(exe->exe.old) = MoveFileExW(REPLACE_EXISTING) must delete the mapped .old -> win32 err 5 -> whole apply fails + rolls back FOREVER, context-free. FIX: (i) DISPLACE not replace — when .old exists, rename it aside to a unique suffix (rename succeeds on mapped files; spt's own updater already does spt.exe.old-); GC stale .old.* opportunistically. (ii) Wrap swap io errors with op + path (`rename claude-spt.exe -> claude-spt.exe.old: …`). Unit seam exists (crc_swap tests). See triage C1. Doc: B) REQ-UPDATE-FINISH-COMMUNE-FLUSH Title: DEFERRED (post-LIFECYCLE-TRUTH, operator-ruled 2026-07-07 — mint now, impl a FUTURE milestone): make the update swap LOSSLESS for live hosted endpoints by flushing a final echo-commune per endpoint BEFORE the brain-subtree reap. ROOT (operator-surfaced probing --finish): `update apply --finish` = daemonless swap -> daemon RESTART; the graceful `daemon stop` path (daemon.rs:316-325) raises brain_stop then reaper.reap() KILLS the brain subtree (brain + shellwake watchers + detached Psyches) as one unit — there is NO per-endpoint final commune before the kill. ENDPOINT-SURVIVAL (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) then RESPAWNS each orphaned online spt-hosted endpoint, but from its LAST commune (whatever the ongoing per-event echo-commune cadence last saved), NOT an as-of-swap checkpoint — so mid-turn / uncommuned work is lost across the bounce. Today's mitigation is operator discipline: commune-before-swap. FIX (future): the stop/finish path, before reap, drives each LIVE hosted endpoint's final echo-commune (fire_echo final context save) so the respawn resumes from a swap-fresh checkpoint. Composes with ENDPOINT-SURVIVAL (commune -> reap -> respawn) and the W1 echo pipeline (REQ-ECHO-DROP-DIR-RESOLVE / REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS). Bounded + loud per endpoint (a commune that hangs must not wedge the stop — timeout then reap anyway, never block the swap). Int: a live hosted endpoint with uncommuned state -> --finish -> respawned endpoint's digest/psyche reflects the pre-swap state (RED-first: without the flush the respawn shows only the last-cadence commune). Doc: PAIR 119: REQ-HAZARD-BROKER-FLOOR-LOCK-POISON <-> REQ-UPD-8 ================================================================================ A) REQ-HAZARD-BROKER-FLOOR-LOCK-POISON Title: B-1 (REMOTE-TRUTH triage §B-1, PIVOTED @e5eb99a): NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a brain-only self-update keeps the broker + all its Mutexes ALIVE (REQ-UPD-3), so a single panic under one poisons it PERMANENTLY: the next `.lock().unwrap()` panics, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines ('brain IPC read deadline elapsed') while non-locked ops keep working. TRIAGE-DRIFT (sweep-dispatch-site-counts discipline): the triage named 3 sites (broker.rs:1163 flush_inject_floor / :1297 inject-worker-open / :2142 buffer_if_held) as the surviving class, but ALL 3 are the INJECT FLOOR and were ALREADY poison-proofed by REQ-HAZARD-INJECT-WORKER-POISON (lock_floor, shipped post-triage — the FLOOR HALF is SUBSUMED, this seed redirects). The SURVIVING class (matching the triage's own symptom description) is the ATTACH-PATH lock set: self.sessions Mutex ×18 + its sessions_exit alias ×1, the per-session OutputLog RING ×11 (log/h.log/log_drain/log_exit), pair_holds ×4 — 34 production bare .lock().unwrap() (cfg(test) excluded). FIX (doyle B-1 ruling): recover ALL THREE via ONE shared `recover(&Mutex) -> MutexGuard` helper (into_inner idiom, same as lock_floor / the effect journal bug #16 — safe for the short coherent-on-recovery map ops of sessions/pair_holds), plus `recover_log(&Mutex)` for the ring which adds a COHERENCE CLAMP on the poison-recovery path: a panic mid-append can leave the ring torn (over-cap, a last seq not below next_seq, non-monotonic front/back) and serving those bytes risks garbage, so OutputLog::clamp_or_reset cheap-checks the invariants and RESETS the ring empty (next_seq preserved — cursors never rewind) + loud-logs on violation. Rationale: fail-fast on the log reintroduces the very wedge B-1 kills (poisoned log = every subscriber attach panics forever); blind recover serves torn bytes; clamp-or-reset costs only scrollback that self-heals on the next PTY output + repaint (lost scrollback << permanent wedge, torn-serve eliminated not tolerated). Sessions/pair_holds recover bare (short map ops, coherent-on-recovery). CLASS invariant (KNOWN-HAZARDS 7.33): any new broker-resident lock uses recover/recover_log or a documented fail-fast justification. Red-first: a scripted panic-under-sessions-lock → recover hands back a usable guard, the next attach still opens; a TORN-RING variant → recover_log clamps/resets so the subscriber gets sane bytes. Doc: 7.33 NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a poison permanently wedges every attach `[REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]` Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → "brain IPC read deadline elapsed") while B) REQ-UPD-8 Title: 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. Doc: PAIR 120: REQ-HAZARD-INFO-RMW-LOST-UPDATE <-> REQ-UPDATE-GH-TRANSPORT ================================================================================ A) REQ-HAZARD-INFO-RMW-LOST-UPDATE Title: Concurrent info.json writers must serialize under the per-perch lock (5.16): an unlocked whole-record write racing a locked RMW is a silent lost update Doc: 5.16 Unlocked whole-record info.json write races a locked RMW → silent lost update `[REQ-HAZARD-INFO-RMW-LOST-UPDATE]` Failure:** `mutate_info` serializes its read→mutate→write under the per-perch `.info.lock` sentinel, but `establish_perch` (`spt::api::startup`) did read→conflict-check→`write_info` with **no lock**. At bind the two writers race (~700µs apart): the daemon RMW reads the PRE-BIND record, bind's `write_info` renames the full record in (`state=live_agent`, `controllable=Some(true)`, `session_id`), then the RMW writes its STALE pre-bind snapshot back plus a `status=online` stamp. T B) REQ-UPDATE-GH-TRANSPORT Title: THE-FORKENING W1 (ADR-0036, operator-ruled 2026-07-14): the release channel is PRIVATE (`BigscreenVR/spt-bs-releases`) and the gh CLI is the mandated carrier — release discovery (`releases/latest`, cli.rs:9717) and asset download (cli.rs:4861 public browser URLs) move to deadline-wrapped `gh` subprocess calls (`gh api`, `gh release download`; run_git pattern). WHY gh not token+HTTP: private-repo `browser_download_url` 404s even with a valid token — the API asset-id dance is gh's job. Default repo flips via the existing SPT_INSTALL_REPO seam (cli.rs:5363) + xtask REPO const (main.rs:729) + notif.rs consent-changelog URL rider. Loud failure classes: gh missing -> UPDATE_FETCH_REJECTED:GhCliRequired with OS-SPECIFIC install hints (winget/apt/brew); gh unauthed -> distinct GhAuthRequired pointing at `gh auth login`. Signature verification unchanged — bytes verified after download, carrier-independent (update-set/counter/anchor continuity per ADR-0036 §2). release_verify_e2e reworked to the gh carrier. Gate: unit — url/invocation construction + both failure classes render OS-correct hints; int — fetch against a real gh-authed channel resolves latest + downloads and verifies an asset; doc — self-update docs name the gh prerequisite. Kin REQ-INSTALL-BOOTSTRAP-VERB (same carrier at first install), ADR-0036. Doc: release channel (private, gh-carried)** — the release channel is a **private** GitHub repo (`BigscreenVR/spt-bs-releases`, ADR-0036); the **gh CLI is the mandated carrier** for release discovery and asset download (each node authenticates via org membership). A node without an authed `gh` cannot fetch — refused loud with OS-specific install hints, never a silent hang. Signature verification is carrier-independent: bytes are verified after download exactly as before; counter, signing key, and update-set format are unchanged from the public-channel era. / Prerequisite: the GitHub CLI.** The rele PAIR 121: REQ-HAZARD-UPDATE-ROLLBACK <-> REQ-UPD-2 ================================================================================ A) REQ-HAZARD-UPDATE-ROLLBACK Title: Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5) Doc: B) REQ-UPD-2 Title: All binaries signature-verified before handoff Doc: PAIR 122: REQ-REL-2 <-> REQ-UPD-7 ================================================================================ A) REQ-REL-2 Title: Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline Doc: M6-D6/D7 (ADR-0015). How a release ships. CI builds; the maintainer signs locally — release keys never enter CI. B) REQ-UPD-7 Title: 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. Doc: PAIR 123: REQ-UPD-1 <-> REQ-UPDATE-DEFAULT-COMPOSITE ================================================================================ A) REQ-UPD-1 Title: Peer-propagated update over P2P Doc: B) REQ-UPDATE-DEFAULT-COMPOSITE Title: THE-FORKENING W4 (operator-grilled 2026-07-14): plain `spt update` = `update fetch --apply` THEN `update adapters` (core-first doctrine order); when core is already current the core leg no-ops and ONLY adapters update; `--core-only`/`-c` skips the adapters leg. GROUNDING (operator-corrected, code-confirmed): fetch --apply cycles the BRAIN only — broker + PTYs survive (apply_staged applyhost.rs:303; the restart-required text is a NOTICE, cli.rs:4615, not behavior) — so the composite's invoking process survives by construction and NO re-run machinery is needed; on a broker-side release the existing F-025 notice remains the composite's closing output. Gate: unit — composite sequencing incl. already-current -> adapters-only and --core-only skip; int — composite on a staged release applies core then updates a registered adapter in one invocation; doc — reference + self-update docs present plain `spt update` as the primary form. Kin REQ-UPDATE-ADAPTERS-VERB, REQ-UPDATE-RESTART-SAFE-SWAP, REQ-UPDATE-APPLY-RESTART-NOTICE. Doc: update composite (`spt update`)** — the plain verb is the primary form: `update fetch --apply` then `update adapters` (core-first order); with core already current, only adapters update. `--core-only`/`-c` skips adapters; `spt update adapters [[,…]]` is the adapters leg alone (alias over `spt adapter update`). The composite's invoker always survives, because a routine apply cycles only the **brain** — the *restart-required* message on broker-side releases is a notice, not a restart. `spt update --restart` is the one-step **full cycle**: fetch → adapters → `apply --finish` last (the finis PAIR 124: REQ-UPD-9 <-> REQ-UPDATE-ADAPTERS-VERB ================================================================================ A) REQ-UPD-9 Title: `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). Doc: adapter update declaration** (manifest field): Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0 B) REQ-UPDATE-ADAPTERS-VERB Title: THE-FORKENING W4 (operator-grilled 2026-07-14): `spt update adapters [[,...]]` = thin ALIAS over the existing `spt adapter update` engine (cli.rs:748 gh_release avenue; the old verb STAYS — published surface) + comma-list accepted on BOTH forms. Semantics: no names -> all gh_release-avenue registrations; names validated FAIL-FAST against the registry BEFORE any update starts (a typo must not leave a half-updated set); per-adapter failure ISOLATION (one failure doesn't stop the rest) with a per-adapter summary line; nonzero exit if any failed; local-path/dev registrations SKIP loud (not error). Gate: unit — name validation, list parsing, isolation + exit-code aggregation, local-path skip; doc — reference regen (drift-gated). Kin REQ-UPDATE-DEFAULT-COMPOSITE (the caller), REQ-ADAPTER-UPDATE-MESSAGE (per-adapter apply notices ride the summary). Doc: update composite (`spt update`)** — the plain verb is the primary form: `update fetch --apply` then `update adapters` (core-first order); with core already current, only adapters update. `--core-only`/`-c` skips adapters; `spt update adapters [[,…]]` is the adapters leg alone (alias over `spt adapter update`). The composite's invoker always survives, because a routine apply cycles only the **brain** — the *restart-required* message on broker-side releases is a notice, not a restart. `spt update --restart` is the one-step **full cycle**: fetch → adapters → `apply --finish` last (the finis PAIR 125: REQ-HAZARD-BOUNDARY-READY-STRAND <-> REQ-READY-AGENT-RESUME ================================================================================ A) REQ-HAZARD-BOUNDARY-READY-STRAND Title: C-2 (F029, SEAM-2 pinned — B6's SECOND HALF, the live-wake blocker; perri wakep9 vs wakep4 gate-state dump + doyle code trace): at a /clear, CC fires SessionEnd(reason=clear) for the DEPARTING session BEFORE SessionStart; the departing sid STILL matches the perch pin at that instant, so the adapter's [hooks.SessionEnd] → `api session-end` AUTHENTICATES and the soft handler REMOVES the ready marker (+ unregister_address, reporting.rs cmd_session_end:206-207). The subsequent `api boundary` rotates the sid but NOTHING re-writes ready → is_online false → try_spt_hosted_inject Nones on the CLI gate BEFORE any broker RPC → every post-clear force-native (incl. the checkpoint FIRE) reports the generic UNDELIVERED, persistent by construction (no path re-stamps ready outside a real bind). The single differing gate field at every UNDELIVERED instant is ready-absent (info online/controllable/rotated-sid all healthy, translate alive). Paid-for hazard. FIX: cmd_boundary re-stamps the ready marker (+ status online, idempotent) ATOMICALLY with the sid rotation — a boundary PROVES a live successor session on the same harness process; a REAL end has no subsequent boundary so genuine teardown is untouched. See triage addendum C-2. Doc: B) REQ-READY-AGENT-RESUME Title: 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) Doc: `spt endpoint run` is the spt-hosted bringup for BOTH endpoint types** (v0.12.0): The bringup core is **type-agnostic** — the endpoint TYPE is the adapter manifest's concern, not a separate bringup mode. A manifest declaring `[session.psyche_init]` brings up a **LiveAgent** (the daemon reconcile hosts its Psyche); a manifest *without it brings up a **ReadyAgent** (a poll listener, no Psyche — see *ReadyAgent* and the harness-hosted ready bind at the *seed + bind-time resolution* note above). No `--adapter`/picker branch distinguishes them: the daemon live-host reconcile hosts only a perch whos PAIR 126: REQ-ADAPTER-TEMPLATE-KEY-VALIDATION <-> REQ-WORKER-SID-SYMMETRIC-AUTH ================================================================================ A) REQ-ADAPTER-TEMPLATE-KEY-VALIDATION Title: P-1 core half (WORKER-TRUTH triage): `adapter add`/`adapter update` validate every declared role template — command, cwd, and [env] inject values — against the substitution-key catalog (spt-runtime BASE_KEYS + role-specific overrides) and REFUSE registration naming the offending key + role (fail-fast family of the [strings] pointer validation). Field driver: flynn's psyche died on the RETIRED {psyche_dir} key (adapter psyche_resume carried the old psyche_init cwd shape) — a permanent template config fault must die loudly at registration, not at the Nth per-event psyche turn via the 3-strike budget. The catalog stays the single source (runtime.rs FILL_KEYS — 'a catalog key must have a real fill'); validation reads it, never a second list. Doc: B) REQ-WORKER-SID-SYMMETRIC-AUTH Title: W-2 (WORKER-TRUTH triage, operator-ruled 2026-07-06): worker verbs go sid-symmetric with every sibling id-scoped verb — worker-start mints NO token and worker-stop takes NONE (token custody is undue adapter burden, ruling via perri). Registration STORES the sid it authenticated (the parent's sid at start; today cmd_worker_start hardcodes session_id="" — worker.rs:44 — so a sid-authed stop compares against empty and refuses 100%). Stop accepts the parent's CURRENT sid OR the stored registration sid (a /clear between start and stop rotates the parent's sid; either rotation endpoint is honest custody — the REQ-PSYCHE-SID-CUSTODY rotation reasoning). Under the ruling the field adapter's existing emission (worker-stop --session-id ) becomes contract-correct as-is. Publish the frozen verb shape to the docs-site with the landing wave (perri blind-builds from published docs). Doc: `api worker-stop --session-id ` · `api worker-poll --session-id ` PAIR 127: REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP <-> REQ-RESUME-REAP-PRIOR-HARNESS ================================================================================ A) REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP Title: Endpoint-stop and brain-death reconcile MUST reap a brain-less perch's orphan detached Psyche via the cmdline-scoped guard (`psyche_orphan_should_reap`) — the handle-reap (`LiveSet::stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT, because the owning brain is gone (its `psyche_child` handle died with it), and the brain-start scoped-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host calls the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Preserves fail-safe-decline (pid-alive AND exe-basename==psyche-program AND cmdline contains `-psyche`; any unreadable signal DECLINES — a missed dup is bounded, a wrong-kill is catastrophic). This is the orphan-leak half of the perri F-010xF-015 field bug (the unsupervised install-dir Psyche that locked an update); the other half is the psyche own-copy (ADR-0025 amendment). (v0.13.2 W3 (a)) Doc: | # | Invariant | spt-core surface | |---|---|---| | 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown | | 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain | | 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection | | 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update | | 2.4 | gen_start = now() on cold-start + handoff | per-instance generation | | 2.6 | A shell's ONLINE-ness is DERIVED (recorded status AND a not-provably-dead `shell.pid`) — an abruptly-killed binary breaks no link, so `c B) REQ-RESUME-REAP-PRIOR-HARNESS Title: B4 (F028, hall-b diagnosis, verified 0.22.0): `--resume` respawns a SECOND harness onto the SAME session without reaping the first. Observed live: resume of b4421cf9 spawned pid 34432 while gen1 (250376) kept running — two claude.exe stacks, one session id. FIX: resume must reap/refuse when the session already has a live harness. See triage B4. Doc: PAIR 128: REQ-SHELL-2 <-> REQ-SHELL-PERCH-DIR ================================================================================ A) REQ-SHELL-2 Title: 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) Doc: B) REQ-SHELL-PERCH-DIR Title: A shell binary can mechanically resolve where its files land. (flynn spt-alchemy clean-room audit 2026-07-21, doyle code-verified, seed pair item 2, P1 — HARD-GATES flynn's alchemy W4.) TODAY: `spt shell send --file` lands the blob at /files/- and the shell_file frame's path attr is PERCH-RELATIVE (files/...) — but the spawn template substitution keys are ONLY {id}/{adapter_name}/{link_token} (shellhost.rs fill_spawn_command) and the spawned child inherits the BROKER's cwd, so no mechanical perch resolution exists: the binary cannot turn the frame's path into a real file without guessing SPT_HOME layout. DOYLE RULING, both halves binding: the frame KEEPS the perch-relative path (an absolute path in a spooled frame LIES across perch moves and node boundaries — frames outlive layouts); the fix is an ADDITIVE {perch_dir} spawn-template substitution key (opt-in — templates that do not use it are byte-identical, N-1-safe by construction) filled with the shell perch dir so the binary receives its root at spawn and joins the frame's relative path against it. REFUSED, recorded so it is not re-proposed: blessing SPT_HOME layout guessing as an interim contract. Published landing contract rides REQ-SHELL-FRAME-VOCAB's page. Sequence-not-version given to flynn (no version floor before it ships). Gate: doc — the {perch_dir} key on the manifest spawn-template docs + the files/- landing shape on the frame-vocab page; impl — fill_spawn_command passes perch_dir alongside id/adapter_name/link_token; unit — the filled template carries the perch dir as ONE argv element (the multi-word/space path shape the argv-fill hazard exists for) and a template WITHOUT the key is byte-identical pre/post (the N-1 guarantee); int — a real spawned shell binary given {perch_dir} resolves a shell_file frame's perch-relative path to the landed blob and proves the read. Doc: `shell_file` — a landed file PAIR 129: REQ-HAZARD-CONTROL-STAMP-CONVERGENCE <-> REQ-HAZARD-REDISPATCH-CONTROL-STEAL ================================================================================ A) REQ-HAZARD-CONTROL-STAMP-CONVERGENCE Title: Control/viewer stamps must CONVERGE to broker session-table truth for every session-backed endpoint, not merely edge-trigger — the UPWARD companion to the DOWNWARD edge-clear REQ-HAZARD-CONTROL-STAMP-LIFETIME (7.27); same 'stamps == broker truth' family. ROOT (F-026 stamp-gap, hall-b/ball-b): a picker-created endpoint's broker spawn become_controller->stamp_driven_by->set_controlled(true) fires BEFORE the adapter binds its perch (a fresh endpoint has no perch until claude boots + binds), so mutate_info returns NotFound and the edge stamp is SWALLOWED (let _); the adapter's bind then writes InfoJson::new with controlled:false DEFAULT and no later edge re-stamps -> the endpoint reads uncontrolled FOREVER while driven (the #3 display fix is correct but datum-starved on this creation path). FIX: the broker (SINGLE WRITER) re-asserts each live session's control/viewer stamps to session-table truth, DIVERGENCE-GATED (read info; compare driven_by/controlled/viewer_count; write ONLY on diff — no per-poll fsync storm), on the KIND_SESSIONS handler (piggyback: the daemon reconcile + picker poll it, so a fresh perch converges within one reconcile-poll window after bind = the BOUNDED window, no new timer). Event-on-input rejected (an idle controlled session like hall-b never converges). Writes run OFF the log lock (snapshot truth under the lock, converge off it) per the lock-across-effect discipline (KH 7.12/5.16). Doc: 7.29 Control/viewer stamps CONVERGE to broker session-table truth, not merely edge-trigger `[REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]` Failure (F-026 stamp-gap, hall-b + the original ball-b):** a picker-created endpoint (`endpoint run` → new) read plain `ONLINE` in the list + picker while genuinely driven — `info.json` `controlled:false` throughout. ROOT (the UPWARD companion to 7.27's downward edge-clear): the broker spawn path's `become_controller` → `stamp_driven_by` → `set_controlled(true)` fires at SPAWN time, but a FRESH endpoint has NO PERCH yet (the adapter binds it after claude boots), s B) REQ-HAZARD-REDISPATCH-CONTROL-STEAL Title: REDISPATCH-TRUTH W1 (KNOWN-HAZARDS 7.41, hertz field RCA 2026-07-16 — 4/5 endpoints frozen per brain cycle): a fresh dispatcher must NEVER re-serve a terminal stream — a replayed historical Attach must not steal (same-identity silent become_controller, no Displaced) or clear (replayed-EOF detach_session) a LIVE controller. The legitimate same-by successor re-take after a brain restart still silently re-takes: the discriminator is stream LIFECYCLE, never origin identity. Gate: int — production-path regression D1: finished historical Attach + current active Attach, same endpoint/origin; restart target brain only (real run_dispatch_loop rediscovery, NO manual re-serve — the pre-fix e2e bypass is the lesson); prove the historical stream neither takes nor clears the current controller and current input/output stays exactly-once without detach; doc — KNOWN-HAZARDS 7.41. HEAVY nextest group at birth (FLAKE-LEDGER #15). Kin REQ-REDISPATCH-FINISHED-RETIRE (the mechanism), REQ-BRAIN-RESUME-NO-CONTROL-STEAL (the CLOSED session-cursor sibling — different leg), ADR-0038. Doc: 7.41 A fresh dispatcher must NEVER re-serve a terminal stream — historical replay must not steal or clear a live controller `[REQ-HAZARD-REDISPATCH-CONTROL-STEAL]` Failure (paid-for, hertz field RCA 2026-07-16 — 4/5 endpoints frozen, twice in one day):** every brain cycle (`spt daemon refresh`, `spt update` apply — same `applyhost.rs` path) launches a fresh `dispatch::run_dispatch_loop` whose EMPTY per-process `claimed` set enumerates every broker-held peer stream. `NetShared.streams` has NO removal path (`StreamLog::finish` only marks) and the claim condition has no finished filter → the fres PAIR 130: REQ-DAEMON-6 <-> REQ-HAZARD-CONTROLLER-LEASE ================================================================================ A) REQ-DAEMON-6 Title: 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. Doc: B) REQ-HAZARD-CONTROLLER-LEASE Title: RC-RENDER-TRUTH W2 (KNOWN-HAZARDS 7.48 — umbrella conformance seam for ADR-0044): at most one input-capable controller lease per PTY session; takeover revokes atomically and loudly; input is fenced to the active lease; node identity is never a lease. The full hertz 8-step deterministic two-loopback-client broker regression rides verbatim: A subscribes Control from node N and controls; B subscribes Take from the SAME node with a different lease; A receives Displaced{by:N} then terminal stream completion (rc exits via existing PumpEnd::Displaced); output post-takeover reaches B not A; A's Input+Resize post-takeover mutate nothing; B's both apply; controlled/driven_by metadata identifies B with exactly one controller slot; a separate equal-lease/equal-generation replay test proves genuine dispatcher recovery remains silent and never self-displaces. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.48. Doc: 7.48 At most one input-capable controller lease per PTY session — takeover revokes atomically and loudly, input is fenced to the active lease, node identity is never a lease `[REQ-HAZARD-CONTROLLER-LEASE]` Failure (paid-for, hertz same-machine `--take` RCA, field repro 2026-07-16):** terminal A controlled an endpoint; terminal B on the SAME machine ran `spt rc --take`. Local loopback attaches carry only NODE identity, so `resolve_subscribe` computed `same_identity=true` and took the silent successor path for a distinct `--take` — intent never consulted; sink replaced with no `Displaced`, no st PAIR 131: REQ-HAZARD-BROKER-PROCESS-ISOLATION <-> REQ-HAZARD-CONTROLLER-LEASE ================================================================================ A) REQ-HAZARD-BROKER-PROCESS-ISOLATION Title: 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). Doc: 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on B) REQ-HAZARD-CONTROLLER-LEASE Title: RC-RENDER-TRUTH W2 (KNOWN-HAZARDS 7.48 — umbrella conformance seam for ADR-0044): at most one input-capable controller lease per PTY session; takeover revokes atomically and loudly; input is fenced to the active lease; node identity is never a lease. The full hertz 8-step deterministic two-loopback-client broker regression rides verbatim: A subscribes Control from node N and controls; B subscribes Take from the SAME node with a different lease; A receives Displaced{by:N} then terminal stream completion (rc exits via existing PumpEnd::Displaced); output post-takeover reaches B not A; A's Input+Resize post-takeover mutate nothing; B's both apply; controlled/driven_by metadata identifies B with exactly one controller slot; a separate equal-lease/equal-generation replay test proves genuine dispatcher recovery remains silent and never self-displaces. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.48. Doc: 7.48 At most one input-capable controller lease per PTY session — takeover revokes atomically and loudly, input is fenced to the active lease, node identity is never a lease `[REQ-HAZARD-CONTROLLER-LEASE]` Failure (paid-for, hertz same-machine `--take` RCA, field repro 2026-07-16):** terminal A controlled an endpoint; terminal B on the SAME machine ran `spt rc --take`. Local loopback attaches carry only NODE identity, so `resolve_subscribe` computed `same_identity=true` and took the silent successor path for a distinct `--take` — intent never consulted; sink replaced with no `Displaced`, no st PAIR 132: REQ-HAZARD-RESTART-IDEMPOTENT <-> REQ-UPDATE-RESTART-SAFE-SWAP ================================================================================ A) REQ-HAZARD-RESTART-IDEMPOTENT Title: Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14) Doc: B) REQ-UPDATE-RESTART-SAFE-SWAP Title: THE-FORKENING W4 (operator-grilled 2026-07-14; RETIRES findings-backlog seed #12 REQ-UPDATE-ONE-STEP-SAFE-SWAP): `spt update --restart` = the one-step ergonomic path to the SAFE full-cycle swap — fetch -> `update adapters` -> `apply --finish` LAST (lethal-leg-last, ruled: apply --finish restarts the whole daemon incl. broker/PTYs, so it must be the final act — everything completes from ANY invoking context including an spt-hosted session whose PTY dies at that step; accepted cost: a finish FAILURE leaves updated adapters on old-activated core briefly — loud + operator-attended by nature of the flag). Composes with `-c/--core-only` (skip adapters leg). The 0.28.0 wedge lesson closes: the ergonomic one-step no longer picks the riskier path by default for operators who want the full cycle. Gate: unit — flag sequencing incl. lethal-leg-last ordering + -c compose; doc — self-update docs present --restart as the full-cycle form and name the finish-restart consequence. Kin REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-FINISH-COMMUNE-FLUSH (deferred commune-flush rides the same finish path when built), seed #12 (retired by this). Doc: update composite (`spt update`)** — the plain verb is the primary form: `update fetch --apply` then `update adapters` (core-first order); with core already current, only adapters update. `--core-only`/`-c` skips adapters; `spt update adapters [[,…]]` is the adapters leg alone (alias over `spt adapter update`). The composite's invoker always survives, because a routine apply cycles only the **brain** — the *restart-required* message on broker-side releases is a notice, not a restart. `spt update --restart` is the one-step **full cycle**: fetch → adapters → `apply --finish` last (the finis PAIR 133: REQ-ENDPOINT-CYCLE-HONEST <-> REQ-HAZARD-DAEMON-STOP-REAP ================================================================================ A) REQ-ENDPOINT-CYCLE-HONEST Title: REGISTRY-LIFECYCLE W3 (ADR-0041 decision 6, operator deployah stop/run wedge): cycle verbs share ONE liveness authority — the ALREADY_LIVE dup-guard liveness-probes the claimed session client tree before refusing (dead tree means reap + respawn honestly, never a refusal citing a zombie); the shutdown state machine consults the same source so is-it-live has one answer (no ALREADY_LIVE / list-OFFLINE / shutdown-NO_EDGE three-way contradiction on the same endpoint). Gate: impl — probing dup-guard + unified authority; unit — dead-tree claim probes and reaps, live claim still refuses; int — controlled zombie (killed client tree, surviving hosted record) leads to endpoint run succeeding honestly end-to-end; doc — ADR-0041. Doc: 1. **Online is earned, not declared.** A creator may stamp `status=online` only from actual persisted state + hosting authority — never from manifest capability alone. Legacy hybrid rows self-heal at reconcile, but only after a SUCCESSFUL broker query: a broker failure is never interpreted as an empty session set (no mass-offline on a hiccup). 2. **Control cleanup splits from offline classification.** Reconcile clears `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth — regardless of state or controllability — while offline classification keeps its narrow gate B) REQ-HAZARD-DAEMON-STOP-REAP Title: 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) Doc: PAIR 134: REQ-HAZARD-LISTEN-ORPHAN <-> REQ-PSYCHE-EPHEMERAL-DRIVER ================================================================================ A) REQ-HAZARD-LISTEN-ORPHAN Title: W4 (LIFECYCLE-TRUTH, KNOWN-HAZARDS): `api listen --parent-pid N` watches parent liveness and exits loud on parent death. ROOT (mobile-gw RCA): --parent-pid is auth-anchor ONLY — no liveness watch; host death orphans the listener forever -> perch held alive (false ONLINE), EVENTs stream to a dead stdout, dead-owner rebind BLOCKED (recorded pid = the live orphan). FIX: listener watches --parent-pid liveness (Windows: job object or poll; Unix: PDEATHSIG or poll) and exits loud on parent death. flynn's job-object guard (spt-mobile side) stays regardless; filed SPT-CORE-NEEDS §5. Unit: parent-death -> listener exits within one poll window. Doc: B) REQ-PSYCHE-EPHEMERAL-DRIVER Title: W1 (F030, design §3): each psyche-relevant event runs exactly ONE bounded per-event turn through the existing driver stack (psyche_turn_and_relay for outbound-intent events / resume_psyche for session-custody transitions / run_psyche_turn for pure merges) — no resident psyche process exists between events. host_one (livehost.rs:518) STOPS spawning spawn_psyche_owned; the pulse loop stays as the daemon-side scheduler (thread + stop-flag + drop-dir watch correct) but a fire now invokes one bounded turn, daemon-driving every substitution key from daemon-known context (child never self-resolves home/subnet/perch — direction-(a) multi-subnet churn impossible by construction). Turn failures consume a bounded failure budget (C3(b) shape): N consecutive failures → psyche_host_error stamp + cooldown, reset on success; no respawn storm (nothing resident to respawn). Red-first: fire an event on a hosted live endpoint → assert one turn ran (SIDE-EFFECT PROOF FILE — transcript-jsonl asserts are structurally blind, 2026-07-04 rig lesson) and no {id}-psyche process survives the turn. Doc: Psyche**: The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. **Liveness = turns succeed** — never PAIR 135: REQ-PICKER-1 <-> REQ-PRESENCE-LIVENESS-TRUTH ================================================================================ A) REQ-PICKER-1 Title: 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) Doc: B) REQ-PRESENCE-LIVENESS-TRUTH Title: A node's gossiped per-endpoint registry Status reflects real liveness, so a remote viewer never paints a DEAD endpoint as ONLINE. ROOT (confirmed + gated vs CONTEXT.md): registryhost.rs:397-405 advertises a NOT-alive perch as Status::Dormant (the `else` of is_perch_alive), re-stamped every gossip round (never ages to Offline). Design intent GATED vs resting.rs + CONTEXT.md: active/dormant is the MULTI-INSTANCE routing differentiator (active = the bare-id routing target; dormant = a WARM non-target sibling — 'driving ling@laptop makes ling@desktop dormant', resting.rs:97; transitions active→dormant on AttentionShift/Detach). suspended = COLD (session closed, resumable-on-wake) while its NODE is UP. offline = NODE DOWN — NEVER self-gossiped (RestState has no Offline; a live node only ever gossips active/dormant/suspended), remote-inferred via epoch-lease eviction. So labeling a NOT-running perch Dormant is the DEFECT (dormant requires warm/running). PRIMARY FIX (registryhost `else`, not-bound-alive): live-but-UNBOUND (has a live broker session; is_perch_alive is bound-gated) → Active/Dormant (still warm); else (cold, no live session, but its node is up because this very daemon is gossiping) → SUSPENDED — NOT Dormant, NOT Offline (the node is UP; Offline is never self-gossiped). This alone removes the false-ONLINE. dormant keeps gossiping (routing/MRA needs it) but RENDERS as its online flavor (no distinct glyph; the dormant→suspended auto-suspend timer disambiguates recency). The DISPLAY of these states (incl Suspended=gray-filled) is REQ-SUBNET-DISPLAY-PARITY. Design: docs/design/subnet-presence-display.md §A. (next milestone) Doc: PAIR 136: REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE <-> REQ-HAZARD-PTY-INPUT-WRITER-WEDGE ================================================================================ A) REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE Title: SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 stall-evict matures (~15s). REQ-UPDATE-TRIAL-DRAIN-DRIVE (counter-54) makes the candidate DRIVE that reap so it promotes within the 30s window — but at a ~15s wedge-maturity hitch (frozen PTYs during the swap). A CLEAN close makes the conn 'simply absent → drained=false AT ONCE → fast promote' (broker.rs:2699-2700), ELIMINATING the hitch = truly seamless (honors the paradigm the field freeze broke). SUPERSEDES the earlier livehost-reattach framing of 'Fix Y': livehost is SPAWN-FRESH (fresh session uuid/pid per boot, nothing to re-attach — wrong site, and it never ran in the trial window); the correct site is the brain-cycle / update-restart path (a bounded graceful-drain of the outgoing brain BEFORE the kill). Non-trivial: hard-kill → bounded graceful drain; a drain that hangs must NOT wedge the swap (timeout then kill anyway, never block the update). Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (defense-in-depth reap-drive REMAINS for any conn that still black-holes — a peer/relay conn, a drain-timeout kill) + REQ-UPDATE-PROMOTE-DRAINED (the gate) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict). Int: a planned update-restart under a live-agent controller conn → the new candidate promotes WITHOUT waiting the ~15s wedge-maturity (drained reads false immediately, no STALL_EVICT), RED-first vs the current hard-kill-then-reap-drive ~15s hitch. Doc: B) REQ-HAZARD-PTY-INPUT-WRITER-WEDGE Title: Pasting into an `spt rc` session WEDGES the broker — after a paste the operator can no longer type AND can no longer attach to NEW or EXISTING sessions (`brain IPC read deadline`). ROOT (doyle /diagnose, code-grounded): the operator-keystroke path rc -> net-stream Input -> serve_attach (attach.rs:197 brain.send_effect) -> KIND_INPUT -> broker dispatch loop (broker.rs:1091) -> dispatch_input (broker.rs:1459) -> session.write_input(&bytes) runs SYNCHRONOUSLY on the broker request-handling thread. W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) released the journal lock across the effect (fix 1) + made PtyWrite ephemeral/no-fsync (fix 3) but EXPLICITLY DEFERRED fix (2) — bound/fail-fast the PtyWrite itself. A single keystroke never fills the ConPTY input buffer; a PASTE BURST does -> write_input blocks -> the dispatch thread cannot service the next frame (a re-attach subscribe, a become_controller restore-write, an inject-floor flush) -> wedge. Not a bug-2 regression (the byte path funnels to the same write_input; paste just reliably fills the buffer). FIX (doyle design, V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md, CONTEXT L33 broker-owns-PTY/minimal + L435 SessionSurface + single-writer pattern): one dedicated per-session INPUT-WRITER THREAD = the SOLE caller of the blocking write_input, fed by a BOUNDED FIFO channel; every caller (dispatch_input, serve_attach->send_effect, inject-floor flush) ENQUEUES + returns immediately, never blocks. A blocked/slow harness blocks ONLY its own writer thread, never the broker dispatch. Backpressure (operator ruling): queue full => DROP excess input + stamp the session INPUT_BACKPRESSURE (visible health signal); the daemon NEVER wedges; a merely-slow harness self-heals as the writer drains. Exactly-once preserved (PtyWrite ephemeral: apply_once effect = the non-blocking enqueue => Applied; ack now means accepted+ordered, benign — rc does not gate on landing); order preserved (single FIFO + single writer); inject-floor (W2 Layer C) choreography moves to the lone writer. Completes the W1b-deferred fix (2), cross-platform (cfg(unix) forkpty park folds in). (v0.13.0) Doc: PAIR 137: REQ-MSG-DELIVERY-AXES <-> REQ-MSG-ENVELOPE ================================================================================ A) REQ-MSG-DELIVERY-AXES Title: Activity-gated inbound delivery + per-message send control as THREE ORTHOGONAL AXES plus opaque metadata (ADR-0028; grilled w/ operator 2026-06-23). SUBSTRATE (the legacy-SPT parity gap, scaffolded-but-unwired today: `delivery::is_idle` + `resolve_inject_methods` exist but the result is discarded `let _methods`, and `broker::dispatch_endpoint_input` injects unconditionally — its comment calls activity-gating 'a deferred follow wave'): an inbound message has an ACTIVE window (endpoint active → spool for the receiver's hook-poll, non-disruptive) and an IDLE window (idle/idle-transition → deliver immediately: translation binary spt-hosted → relay-poll either topology → spool, in fallback order). AXES (each composes; each defaults to its unrestricted value): (1) DELIVERY WINDOW — default (both, first-to-fire) | `--idle-only` (idle window; immediate if already idle) | `--active-only` (active window only, never wakes; the RENAMED `--deferred` — `deferred=1` spool column + `api poll --include-deferred` keep their names). (2) CHANNEL RESTRICTION — unrestricted | `--prefer-native` (translation binary if running else fall back) | `--force-native` (binary ONLY, no fallback/no spool-to-other-method). Native flags do NOT respect the binary's idle-gating: the WINDOW says when, the native flag says through-what (so `--force-native --active-only` = binary injects during the active window, mid-turn-safe via the existing InjectFloor). (3) PERSISTENCE — durable (default; spool until delivered or TTL) | `--ephemeral` (drop if undeliverable in the accepted window — at window-open with no live carrier, or at TTL, whichever first). METADATA (orthogonal): `--json-payload ''` → a single attr-escaped `json="…"` envelope attr ALONGSIDE (not replacing) the body, pure verbatim passthrough across spool/TCP/WAN/EVENT-PART, parsed only by the receiving adapter; collision-proof by construction (structured data lives INSIDE the one `json` value, can never forge `from`/`type`); available to ANY sender (confers no spt-core authority). HAZARD: `--ephemeral` is the ONLY path permitted to drop silently — the sender-opted-in carve-out to REQ-HAZARD-IDLE-SILENT-NONDELIVERY (that hazard gains a '…unless --ephemeral' clause in v0.15.0). (v0.15.0) Doc: Activity-gated message delivery + send-modifier axes B) REQ-MSG-ENVELOPE Title: 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. Doc: Decision PAIR 138: REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE <-> REQ-RESUME-ROW-PER-PROJECT ================================================================================ A) REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE Title: 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) Doc: B) REQ-RESUME-ROW-PER-PROJECT Title: A5 (F028, operator #6): resume-from-history labels EVERY session with the endpoint's newest project. data.rs:480-496 resume_rows_for clones project_history.first() onto every ResumeRow (line 481/488), so all sessions read as the head project (the ghost). The per-row e.cwd is already carried for launch-into-dir. FIX: derive per-row project_id_for_dir(e.cwd) (owlery-excluded -> fall back to trigger token), rendered through A1's display-name path. See triage A5. Doc: PAIR 139: REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE <-> REQ-PUMP-DIAL-FASTFAIL ================================================================================ A) REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE Title: A dispatcher-SEEDED attach Request is honored ONCE — the wire replay's copy of that same Request must not re-establish it. (Seed: DAEMON-LIFECYCLE W1 build, answering ADR-0047 decision 3's banked open question; origin CODE-PROVEN, not inferred.) THE ORIGIN: serve_attach (spt-daemon/src/attach.rs) processes the dispatcher-pinned `seed_request` through handle_attach_request BEFORE its event loop (ADR-0038 B, the durable-opener seed — it exists because a ROLLED ring can no longer replay the Request). It then subscribes the stream from the floor and enters the loop, whose `AttachRecord::Request` arm calls handle_attach_request UNCONDITIONALLY — there is NO `!attached` guard, and NO record that the seed already established this exact Request. When the ring has NOT rolled, the wire replay still carries that Request, so the SAME record is handled TWICE on the SAME conn, milliseconds apart, with identical (session_id, from_seq, intent, gen, identity) — exactly hertz's v0.39.4 bug-4 capture (conn1011, 15ms apart, post-refresh replay). The two arms are the same function on the same seed record; the seed is pinned by the dispatcher on the post-cycle replay, which is the field trigger. SIBLINGS to check at triage: any other serve family that seeds an opener record AND re-reads it from the replay (xfer/rest seeds), and whether a re-established seat should also re-run the access check. FIX SHAPE (not built, W1 deliberately shipped the broker-side idempotence instead): the serve worker knows it already honored a Request — a guard on the loop's Request arm (already attached + same record = ignore), which fixes the ORIGIN rather than absorbing it at the broker. The broker idempotence stays regardless: it closes the symptom for EVERY origin, including origins not yet found. A serve-path int rig replaying the REAL double-serve shape belongs to this REQ, not to W1 (doyle ruling 2026-07-22: do not simulate a behavior we have not explained). Kin REQ-ATTACH-IDEMPOTENT-REPLAY (the broker-side absorber), REQ-STREAM-OPENER-DURABLE (why the seed exists), ADR-0038 B. Doc: B) REQ-PUMP-DIAL-FASTFAIL Title: PUMP-TRUTH W1 (RE-SCOPED post round-2 empirical lock — the DIAL is EXONERATED, healthy ~100ms): a pump worker-leg PEER-REPLY read to a connect-then-silent / half-alive peer must drop THAT peer as an ORDINARY per-peer failure (peer_outcome's non-TimedOut arm -> PUMP_PEER_FAIL -> drop conn + redial, round CONTINUES, heartbeat advances), NEVER burn the brain's 30s PUMP_PEER_IO_TIMEOUT carrier deadline into a whole-round TimedOut POISON -> supervise_pump doubling-backoff restart. ROOT (deployah leg-instrumented capture, enlyzeam, 3 identical rounds): DIAL_EXIT 96ms ok, LEG i=3 update ms=30025 err[TimedOut] = the wedge. request_update (propagate.rs:373-375) opens the update stream + sends UpdRecord::Query (all bounded, all land), then BLOCKS read_event_until(deadline=call_deadline()=30s) on the peer's Offer/UpToDate reply; a peer that accepts the stream but never answers burns the full 30s -> TimedOut -> peer_outcome (pump/mod.rs:601) POISON -> whole-round abort + restart (= the field PEER_PUMP_FAIL: brain IPC read deadline, always-zero PUMP_PEER_FAIL). request_sync (sync.rs:374-376) is the LATENT TWIN (SKIPS the reply-read only when the want-set is empty; bites the moment it is non-empty against a silent peer). FIX (both legs): (a) reclassify the reply-read no-progress timeout OUT of TimedOut to a non-poison kind (Brain::read_peer_reply_until) so peer_outcome drops ONLY that peer -- poison RESERVED strictly for a genuine broker-IPC-CARRIER desync (the carrier ops net_open_stream/subscribe/send keep raw TimedOut); the abandoned peer stream is safe (exactly-once seq cursor stays contiguous, a late reply matches no live stream id). (b) budget-decouple the reply-read below 30s (Brain::reply_read_deadline = now + min(io_timeout, 10s)) so a silent peer drops promptly even in the still-sequential pre-W2 pump and can never race the carrier deadline. Files: propagate.rs (request_update) + sync.rs (request_sync) + brain.rs (reply_read_deadline + read_peer_reply_until) + pump/mod.rs (peer_outcome poison reserved for carrier-desync). Gate: a connect-then-silent peer at fan#0 -> the update leg drops it ordinarily within the reply-read budget, round continues + heartbeat advances, NO PEER_PUMP_RESTART; happy path (live peer) unchanged; + the sync-non-empty-want-set latent case. Kin REQ-PUMP-PEER-ISOLATION (W2 concurrency, VALIDATED by this root) + REQ-HAZARD-PUMP-IPC-DEADLINE (the poison it must stop mis-firing on a peer). Doc: PAIR 140: REQ-HAZARD-REGISTRY-STALL <-> REQ-REDISPATCH-FINISHED-RETIRE ================================================================================ A) REQ-HAZARD-REGISTRY-STALL Title: REGISTRY-LIFECYCLE W1 (KNOWN-HAZARDS 7.44, hertz post-close v0.36 RCA — the umbrella conformance seam): streams and seats on a long-lived connection have BOUNDED lifetime. The hertz regression seam verbatim: real long-lived pump + dispatcher integration, N registry-only rounds over ONE persistent connection, asserting (a) dispatcher-eligible Registry rows plateau O(active) not O(N); (b) physical stream/subscriber/seat counts plateau after completion CROSS-FAMILY (sync/update seats included, not just Registry); (c) snapshot writes O(feeds) not O(chunks x record-kinds); (d) brain refresh produces ZERO historical Registry replay subscriptions; (e) zero CONN_WRITE_POISONED / replay-write-failed events; (f) broker thread count returns to a bounded baseline. HEAVY nextest group at birth (spawns a real daemon tree). Binding: redispatch D1/D1b + REDISPATCH-STALL T1-T7 + mesh-recovery legs green every leg (retire machinery + registry gate = substrate). Gate: int — the seam above; doc — KNOWN-HAZARDS 7.44. Doc: 7.44 Streams and seats on a long-lived connection must have bounded lifetime — one-way rows terminal at FIN, seats released at serve completion, no per-chunk full-state rewrites in a drain loop `[REQ-HAZARD-REGISTRY-STALL]` Failure (paid-for, hertz post-close v0.36 field RCA 2026-07-17 — live prod box):** FOUR compounding consequences of "physical teardown waits for conn close" on connections that never close. The registry pump opens one fresh stream per feed (~30s) on the persistent pump conn; nothing retires the rows (the dispatcher — sole `retire_stream` caller — skips `initiated_locally`) B) REQ-REDISPATCH-FINISHED-RETIRE Title: REDISPATCH-TRUTH W1 (ADR-0038, hertz fix A): finished/terminal stream rows are RETIRED from redispatch eligibility — NetShared.streams today has NO removal path (single insert nethost.rs ~649; StreamLog::finish only marks) so every dispatcher generation re-enumerates every historical stream forever. Retire terminal rows from the enumeration the dispatcher claims from (remove, or lifecycle-exclude), preserving only the post-EOF state genuinely needed by other readers (presence/log reads); bounded growth replaces forever-discoverable rows. Clearing the whole table on brain restart is REJECTED (destroys live streams' reconstruction facts). Gate: impl — the retirement path; unit — a finished stream is invisible to the dispatch enumeration while an active one stays claimable + post-EOF reader state survives retirement; doc — rides ADR-0038 + the triage doc. Kin REQ-HAZARD-REDISPATCH-CONTROL-STEAL (the invariant it satisfies), REQ-STREAM-OPENER-DURABLE. Doc: Decision PAIR 141: REQ-STREAM-LEASE-CLASSES <-> REQ-STREAM-LIFETIME-CLASS ================================================================================ A) REQ-STREAM-LEASE-CLASSES Title: REGISTRY-LIFECYCLE W2 (ADR-0040 decision 6 + ADR-0041, emphasys C2 P0): streams declare a lifetime class at open — RC attach/view streams are ConnectionBound (opener conn EOF means the target sees FIN, serve_attach runs detach_session, controller slot + CONTROLLED stamps clear — a dead viewer can never pin a controller across its own connection death; today the raw-closed viewport attach stream is restart-durable forever); inter-brain streams stay Durable (NEVER globally retire on Brain disconnect — brain-swap correctness depends on it). Late-close identity validated (stale opener A close cannot evict newer controller B — rides ADR-0038 Amendment fix-6 generation tokens + W1 seat teardown machinery; same neighborhood, built once per the standing C2 coordination ruling). Lifetime class = additive open field, absent = Durable (N-1 openers keep exact current semantics). Gate: impl — class at open + ConnectionBound EOF chain; unit — class routing + absent-defaults-Durable + late-close identity refusal; int — raw viewport close frees the controller full-chain incl. across broker restart, brain_swap/daemon_refresh/redispatch legs stay green; doc — ADR-0040/0041. Doc: 1. **One-way (fire-and-forget) stream families are terminal at FIN, sender-side.** The registry feed pump retires its own row after successful write+FIN via the existing `net-stream-retire` verb (best-effort on N-1 brokers, per ADR-0038 A). A one-way family's exchange is definitionally over at FIN; keeping the row eligible reproduces the O(history) defect forever. 2. **Eligibility filtering is server-side.** `stream_infos` excludes `initiated_locally` rows (alongside `retired`) before serializing. Consumers keep their client-side guards (double-filter harmless; N-1 compatible both directions). B) REQ-STREAM-LIFETIME-CLASS Title: RESCOPED at W2 activation 2026-07-22 (doyle verify-first, C3-retirement precedent): the CLEAN-CASE chain this REQ was minted for is ALREADY SHIPPED under REQ-STREAM-LEASE-CLASSES / ADR-0040 decision 6, verified against @7c0f12d — StreamLifetime is opener-declared and on the wire, serde-default Durable so N-1 openers keep exact today-semantics (msg.rs:810-836 + the additive-wire unit @msg.rs:1408); rc attach/view is the SOLE ConnectionBound opener (attach.rs:667-677); the broker binds the class at open (broker.rs:5689) and nethost carries it per StreamEntry (nethost.rs:1059-1076); the conn-exit sweep FINs each ConnectionBound stream toward its target and terminal-retires the local row while Durable rows never enter it (broker.rs:4118-4123/4227-4235/4429-4446); the target's serve_attach EOF arm runs detach_session_gen(serve_gen) so controller/viewer stamps clear on exactly one generation (attach.rs:580-597); the late-close gen guard (broker.rs:2179-2192, unit @8416) and converge_perch_stamps close the race; int coverage rides endpoint_lifecycle.rs:241. Building that again would re-implement shipped code — the C3 lesson. WHAT REMAINS, and what this REQ now owns: RESTART-REPLAY RE-ESTABLISHMENT. ADR-0038 dispatch replay re-serves every retained opener Attach across a dispatcher generation, and a ConnectionBound opener REPLAYING is a class contradiction — the conn that declared the class cannot exist after the restart that killed it, so re-serving resurrects a seat whose declared lifetime already ended. FIX: the dispatcher's restart-durable classification (ADR-0038 decision 2, dispatch.rs first_line) gains lifetime, and the replay filter TERMINAL-RETIRES ConnectionBound openers instead of re-serving them. Deliberately PRECISE, not ADR-0038's rejected clear-table-on-restart: Durable/brain-swap semantics are untouched and the brain_swap / daemon_refresh / redispatch D1+D1b families staying green IS the proof. The filter runs UPSTREAM of the W1 idempotence key (endpoint/session, by, conn, gen, from_seq) — a filtered opener never reaches serve, so the two mechanisms compose rather than overlap. Gate: impl — lifetime on the replay classification + the terminal-retire filter; unit — the filter predicate (ConnectionBound retires / Durable replays / unclassifiable stays terminal as today); int — RED-first: kill the rc opener raw, restart the broker, pre-fix the seat and CONTROLLED stamps re-establish via replay, post-fix they stay clear, with the D-legs green alongside. Kin REQ-STREAM-LEASE-CLASSES (the shipped clean case), REQ-ATTACH-IDEMPOTENT-REPLAY (downstream), REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (the live no-FIN sibling, same wave). RESCOPE 2 — MEASURED 2026-07-22 (todlando, Leg A rig `restart_replay_lifetime.rs`, GREEN with its sibling probe passing): the FIX ABOVE IS NOT OWED. The premise was pre-registered as falsifiable by doyle BEFORE the rig was built, and the rig falsified it: in the residual ordering (dispatcher generation dies FIRST so no serve worker and no Served report, THEN the rc opener dies RAW with no detach frame) a fresh generation does NOT resurrect the seat — stamps stay clear and no DISPATCH line touches the dead opener's row. The population is closed BY COMPOSITION: the conn-exit sweep fires for this kill shape (`STREAM_CONNBOUND_RETIRE:: opener conn exited — FIN + terminal retire`), the ConnectionBound opener row leaves the table entirely, its peer row carries the FIN as finished=true, and the worker's family-aware finished_row_is_terminal(Attach,true) retires it UNSERVED. TWO PRE-REGISTERED SUB-QUESTIONS ANSWERED, and the first is the non-obvious finding: (1) `retire` does NOT exclude from the CLAIM condition on this path — the dead opener's row is STILL in the dispatcher's claimable enumeration, merely finished=true, so what stops the replay sits DOWNSTREAM of the claim (retire-on-sight in the worker), which is ADR-0038 decision 1's `excluded by lifecycle state` arm and NOT its `removed from the enumeration` arm; (2) the sweep DID run for this kill shape. Consequently NO wire field (already settled independently: the re-served row registers Durable, class-blind by design at nethost.rs:1028-1041 / 1763-1771, so a lifetime field on the opener reply would report Durable for exactly the population a filter exists to catch) and NO broker-side enumeration filter are built. Stages drop to int ONLY — impl/unit are deactivated rather than pre-failed (the `activate, don't pre-fail` rule; the C2/C3 verify-first precedent), and the int evidence is the standing regression guard the property never had. SCOPE LIMIT STATED, NOT IMPLIED — and TIGHTENED 2026-07-22 (doyle rider, todlando self-flagged): the measurement rides the IN-PROCESS DUPLEX, which is the honest word and is tighter than the `loopback` this text first said. net_dial_loopback / open_loopback_pair yield a RecvHalf::Loopback duplex with NO QUIC ANYWHERE and NO idle timeout — a different transport leaf from RecvHalf::Quic; BindScope::Loopback plus a real net_dial WOULD be real QUIC over 127.0.0.1, and the two senses of `loopback` in this repo are NOT the same thing (the naming collision is itself an ADR-0040 amendment item, so future rigs pick their venue deliberately). Consequently the cross-node `conn.closed()` arm (nethost.rs:1000-1021) is a different mechanism reaching the same outcome and is NOT exercised, and a real-QUIC transport death sans FIN remains Leg B's instrument-first residual under REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. Doc: Amendment 1 (2026-07-22, DAEMON-LIFECYCLE W2) — teardown authority is opener-declared class PLUS transport liveness, enforced at the three places decision 6 could not see PAIR 142: REQ-HAZARD-REGISTRY-DIR-CREATE <-> REQ-SUBNET-DUAL-SEED-MINT ================================================================================ A) REQ-HAZARD-REGISTRY-DIR-CREATE Title: SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9) Doc: 4.9 SQLite stores must create their parent dir — SQLite won't Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — "unable to open database file …owlery\.registry". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04 B) REQ-SUBNET-DUAL-SEED-MINT Title: `subnet create` mints TWO TOTP seeds in one ceremony — the member key (today's subnet seed, unchanged) and a second admin key — and provisions both into the creator's authenticator at that single moment (ADR-0051 decision 1, CONTEXT.md 'member key / admin key'). An admin key IS a membership key; a member key is NOT an admin credential. The same ceremony captures the subnet's universal control-surface mode: prompted with NO PRESELECTION (an operator must state open or closed rather than accept a default that a hurried Enter would pick for them), with `--open`/`--closed` bypassing the prompt for scripted creation. This is the moment the whole two-key model depends on: the admin seed has no reveal verb ever (REQ-SUBNET-ADMIN-SEED-REPLICATION), so a creation path that mints it without displaying it, or displays it without persisting it, permanently destroys the subnet's admin authority with no recovery but re-minting the subnet. Gate: doc — the CONTEXT.md two-key entry and ADR-0051; impl — dual mint at create, both authenticator provisionings, the no-preselection mode prompt and its flag bypass; unit — creation yields two DISTINCT seeds, the mode is recorded from prompt or flag, and no code path yields a subnet holding one seed. Doc: member key / admin key (two-key subnet)** (ratified 2026-07-28, access-control grill): Subnet creation mints **two** TOTP seeds. The **member key** is today's subnet seed (join ceremony, show-code under elevation). The **admin key** is a second seed: it also joins its origin subnet (an admin key IS a membership key; a member key is NOT an admin key), and it additionally gates subnet-scope access administration (see *empower*). **Held everywhere, revealable nowhere:** every member node holds both seeds (replicated at join — needed to verify `empower` and to serve admin-code joins), but the admi PAIR 143: REQ-SUBNET-ADMIN-SEED-REPLICATION <-> REQ-SUBNET-CREATE-CAPTURE-PROOF ================================================================================ A) REQ-SUBNET-ADMIN-SEED-REPLICATION Title: Every member node holds BOTH subnet seeds — the admin seed replicates at join exactly as the member seed does — because empower verification and admin-code joins are LOCAL operations any member node must be able to serve without consulting a privileged peer (ADR-0051 decision 2; the mesh stays symmetric, authority rides a key rather than a node). Carries the NO-REVEAL invariant, which is the load-bearing half: there is no admin show-code, no QR re-provision, no `--json` field, no debug/diagnostic/error/log rendering of the admin seed ANYWHERE — it is displayed exactly once, at creation, and thereafter no node can leak what no node can reveal. A lost authenticator entry is irrecoverable by design (ADR-0051 decision 3): re-mint the subnet. The invariant binds tests too — a test that asserts the seed's VALUE by printing it is itself the leak (secret-probe discipline: never expand a secret into an observable sink). Gate: doc — ADR-0051 decisions 2 and 3; impl — admin-seed replication on the join path and the absence of every reveal surface; unit — a joined node holds both seeds and can verify an admin code locally, and no CLI/JSON/diagnostic output path emits the admin seed. Doc: 2. Held everywhere, revealable nowhere / 3. Irrecoverable by design B) REQ-SUBNET-CREATE-CAPTURE-PROOF Title: `subnet create` proves the admin key was captured before anything persists (ADR-0051 §2b, FF grill 2026-07-30). Ceremony order, binding: the admin material displays FIRST — labelled otpauth URI + terminal QR, with NO current TOTP code anywhere on screen, because that absence is the proof's soundness (the typed-back code cannot be produced from what the screen shows, only from an authenticator that captured the seed); the human types a current admin code back, accepted within the same ±1-step window the pairing ceremonies use; the admin material is then cleared, scrollback included; and only then does the member material display, at the very end. Creation does not complete without the proof: the mint persists only after it, so a cancel, three wrong codes, or a non-interactive stdin (refused before any material is written into a pipe) leaves NO subnet — never a subnet whose admin key nobody holds, and decision 3's irrecoverability cost stays a choice rather than an accident of a missed scan. Gate: doc — ADR-0051 §2b; impl — the phased ceremony driver, the code-free capture display, the windowed proof check, and the proof-gated persist; unit — the capture display carries the provisioning material and none of the window's current codes, and the proof check accepts exactly the ±1 window. Doc: 2b. Capture proof at create (added 2026-07-30, fast-follow grill) PAIR 144: REQ-DSR-SINGLE-CPR <-> REQ-RC-VT-TEARDOWN ================================================================================ A) REQ-DSR-SINGLE-CPR Title: TEARDOWN-AUTHORITY W3 rider (hertz RCA 2026-07-19): a Device Status Report query yields EXACTLY ONE Cursor Position Report. Rides W3 because it is the same emulator-conformance surface as the width work and the same field capture surfaced it; kept a separate REQ so its evidence is not buried inside the width tags. Gate: impl — one CPR per DSR on the emulator reply path; unit — a DSR in the input stream produces a single well-formed CPR carrying the DISPLAY-column cursor position (i.e. consistent with REQ-SCREENGRID-WIDTH reckoning, not the raw char count). Doc: B) REQ-RC-VT-TEARDOWN Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 2, hertz stale-glyphs RCA leg 2 P0): rc display teardown is a display RAII guard SEPARATE from the OS input/raw-mode guard, unconditional and idempotent on EVERY exit path including errors and unwind — best-effort SGR reset + full scroll-region reset + cursor show + leave alternate screen + clear/home, emitted while VT output processing is still enabled, THEN restore the prior console output mode, THEN parting prose (today RawGuard::drop restores raw/mouse/console-mode only; detach, child exit, displacement, first-event stall, fatal error, and the 30s reconnect give-up all can leave the operator terminal dirty; the reconnect banner clears+homes then give-up prints at the centered cursor). Gate: impl — split display guard + every-path coverage; unit — guard emits the cleanup postlude exactly once, idempotent on double-drop; int — dirty sink (?1049h ?25l SGR31) x every PumpEnd/error class => cleanup postlude precedes the final prose; doc — ADR-0043. Doc: 1. **One FIFO sequencer per attach sink.** The PTY drain/output writer is the sole sequencer for terminal Output and Exit: Exit is enqueued behind all prior output for each sink (drain EOF/completion first, then Exit). A mutex alone is insufficient — producer order is the contract. Output-before-Exit is a production-path invariant, regression-proven end-to-end (broker → attach → rc). 2. **rc display teardown is unconditional, idempotent, and separate from input teardown. A display RAII guard (distinct from the OS input/raw-mode guard) runs on every exit path including errors and unwind: best-e PAIR 145: REQ-RC-NEWLINE-PRESENTATION-TRUTH <-> REQ-RESUME-ROW-PER-PROJECT ================================================================================ A) REQ-RC-NEWLINE-PRESENTATION-TRUTH Title: W3 (v0.39.4 field bug 3 ROOT, pinned 2026-07-22): the rc client presents relayed bytes exactly as the emitter addressed them — KNOWN-HAZARDS 7.56. rc's with_vt_output enables ENABLE_PROCESSED_OUTPUT without DISABLE_NEWLINE_AUTO_RETURN (DNAR: zero grep hits tree-wide), so the operator's console translates relayed bare LF into CR+LF — a column reset the emitter never asked for; the /c/config field scraps are capture record t=1784714152501's bare-LF index presented through that mode. Relay byte-clean end to end (why both isolated rigs read NOT-REPRODUCED and the fresh viewer read clean — the synthesized repaint is CUP-absolute). FIX: DNAR joins the with_vt_output composition whenever processed/VT output is enabled; prior-mode capture/restore unchanged; Unix needs nothing (raw mode already clears OPOST/ONLCR). Regression: the W3 captured-bytes rig PAIR (vehicle console without DNAR = field-client model = RED pre-fix; with DNAR = GREEN post-fix) + the minimal LF column-preservation probe through an rc-mode console. Amends REQ-RC-WIN-VT-OUTPUT (#12); kin KH 7.55 + the W3 vehicle-fidelity clause (three console-mode-seam manifestations, one day). Attribution CLOSED when hertz confirms fixed-rc clean on the field session; no debug channel needed. Doc: Failure (paid-for — v0.39.4 field bug 3, the `/c/config` scraps, pinned 2026-07-22 via the W3 bisect):** rc's Windows viewport enables VT output as `prior | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT` (`rc.rs` `with_vt_output`) and `DISABLE_NEWLINE_AUTO_RETURN` appears NOWHERE in the tree — so with processed output on, the operator's console translates every relayed bare LF into CR+LF. A hosted TUI that emits bare LF as a plain index at a non-zero column (capture record t=1784714152501: `CUP 51;3` + `EL` + SGR + `0x0a` + `"/cd…"`) is presented with a column reset the emitter B) REQ-RESUME-ROW-PER-PROJECT Title: A5 (F028, operator #6): resume-from-history labels EVERY session with the endpoint's newest project. data.rs:480-496 resume_rows_for clones project_history.first() onto every ResumeRow (line 481/488), so all sessions read as the head project (the ghost). The per-row e.cwd is already carried for launch-into-dir. FIX: derive per-row project_id_for_dir(e.cwd) (owlery-excluded -> fall back to trigger token), rendered through A1's display-name path. See triage A5. Doc: PAIR 146: REQ-SCREENGRID-REPAINT-MODE-REPLAY <-> REQ-SUBNET-8 ================================================================================ A) REQ-SCREENGRID-REPAINT-MODE-REPLAY Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 4, hertz stale-glyphs RCA leg 4 P1): ScreenGrid cold repaint replays EVERY tracked render-affecting mode — DECSTBM scroll margins at minimum — before final cursor placement (today render_repaint omits tracked margins, so client and server grids interpret subsequent raw scrolling against different regions => stale/moved rows after reattach/resize; the trailing-blank omission after ED2 is semantically correct and NOT the bug). Stateful emulator contract: dirty screen + synthesized repaint + next raw frame == server grid. Gate: impl — tracked-mode replay in render_repaint; unit — repaint emits tracked DECSTBM, emulator contract holds for scroll-after-repaint; doc — ADR-0043. Doc: 1. **One FIFO sequencer per attach sink.** The PTY drain/output writer is the sole sequencer for terminal Output and Exit: Exit is enqueued behind all prior output for each sink (drain EOF/completion first, then Exit). A mutex alone is insufficient — producer order is the contract. Output-before-Exit is a production-path invariant, regression-proven end-to-end (broker → attach → rc). 2. **rc display teardown is unconditional, idempotent, and separate from input teardown. A display RAII guard (distinct from the OS input/raw-mode guard) runs on every exit path including errors and unwind: best-e B) REQ-SUBNET-8 Title: 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) Doc: PAIR 147: REQ-DIGEST-CROSS-NODE-PULL <-> REQ-PEER-ROUTE-CHAIN ================================================================================ A) REQ-DIGEST-CROSS-NODE-PULL Title: The digest SNAPSHOT PULL crosses nodes: `spt endpoint digest <[subnet:]id@node>` (including `--after ` incremental polling) resolves and serves under the EXISTING address gate — the CROSS_NODE_M4 qualified-address refusal lifts for the pull verb only. The structured-delta stream stays node-local until a real cross-node subscriber exists (ADR-0048 decision 5, defer-until-consumer; operator-ruled 2026-07-24). Access policy is NOT new — fetch was ratified address-gated long ago; this is routing. Composition note: REQ-DIGEST-SEAL-ON-IDLE's stable seqs are what make remote --after polling trustworthy. Forcing customer: spt-mobile 'view live digest of ANY endpoint' (host proxies transparently once the refusal lifts). Gate: int — two-node rig: a qualified digest pull from the non-hosting node returns the hosting node's buffer; --after returns only rows past the cursor; the address gate still refuses a non-resolvable caller. Doc: Reaching an endpoint on another machine.** The snapshot pull accepts a qualified address — `spt endpoint digest --json`, `--last` and `--after` included. The endpoint's own node projects and answers, so the content is identical to what someone standing on that machine would see, and the same access rules apply as for every other qualified address. `--follow` is **local-only**: a delta subscription is not available across nodes, so poll the snapshot with `--after ` instead. A qualified address that resolves to the machine you are already on is answered locally, not over the netwo B) REQ-PEER-ROUTE-CHAIN Title: MESH-RECOVERY W1 (ADR-0039, RCA wave 1): dial-address resolution is the ROUTE CHAIN — exact peer-cache entry, then VALIDATED RosterEntry.address (address.id must match the peer key; a poison row never becomes a route), then id-only discovery — always fully consulted in order (no failure-count heuristics gating legs; rotation machinery REJECTED). Retention is NONDESTRUCTIVE: PRESENCE_DIAL_FAILED demotes the cached route to suspect (skipped in favor of the roster leg while suspect, superseded by any validated fresher address from connect write-back or reconcile), never deletes a sole route; removal only via validated-fresher replacement or roster tombstone. Validated roster addresses RECONCILE into the cache at daemon startup and on roster merge (beyond gapfill's fill-only: validated-fresher replaces failed/suspect rows) — recovery is connection-independent. Amends REQ-CONV-1's drop-on-fail mechanism; peeraddrs.rs/pump doc-comment truth rides the same change. Gate: impl — chain + demote + reconcile; unit — chain order incl. id-mismatch roster row resolves nothing + suspect row survives N failures with no replacement + reconcile replaces suspect with validated-fresher; int — rides REQ-HAZARD-MESH-BOOTSTRAP-TRAP D-legs; doc — ADR-0039 + CONTEXT peer-route chain entry. Kin REQ-PEERADDR-INVARIANT, REQ-CONV-1. Doc: Decision PAIR 148: REQ-GOSSIP-PROJECT-DERIVE-ONCE <-> REQ-WORKER-LIST-VISIBILITY ================================================================================ A) REQ-GOSSIP-PROJECT-DERIVE-ONCE Title: REGISTRY-LIFECYCLE W4 fast-follow (hertz v0.37.0 field-verify CPU-gate FAIL RCA 2026-07-17, doyle seam-verified same day; timing-pinned ~16s burst clusters per 30s Registry pump round): recent-project gossip derives a project id AT MOST ONCE PER DISTINCT CWD per advertisement round — recent_projects_for dedups the RAW cwd string BEFORE project_id_for_dir (today seen.insert(pid) dedups AFTER derivation at registryhost.rs:1001, so every duplicate-cwd sessions.log row pays a git remote/rev-parse spawn: ~200 derivations/round on a 12-perch box, the post-self-hash-fix CPU floor). The pid-level dedup stays as the output-uniqueness second layer (distinct cwd spellings may collapse to one project); ordering semantics unchanged (first-occurrence newest-first); bounded ledger read unchanged. Gate: impl — cwd-dedup before derivation; unit — derivation-count observable == distinct cwds not ledger rows (duplicate-cwd ledger derives once), output/order parity vs the pre-fix shape on a mixed ledger. SEEDED SEPARATELY (not this REQ): gossip leg consults the ADR-0037 materialized index (kills git entirely + cross-round rederivation; staleness-semantics design change) + epoch-mint batching (26 atomic writes/round; durable-monotonic contract change). Doc: B) REQ-WORKER-LIST-VISIBILITY Title: V-1 (WORKER-TRUTH triage, operator rider): worker perches leave the DEFAULT `spt endpoint list` view — they are process-local machinery, not subnet citizens; leaked-or-live worker rows rendering as permanent OFFLINE endpoints is the operator-visible symptom root. A dedicated flag (--workers) reveals them (one command + flag per the --all/--detail precedent, NOT a separate list-working command — sister divergence deliberate). Applies to the human render, --json (additive default-absent filter), and the registry/projection legs; verify-and-stop any worker gossip into the subnet registry as peer endpoints. Doc: PAIR 149: REQ-NET-1 <-> REQ-SHELL-3 ================================================================================ A) REQ-NET-1 Title: WAN messaging first-class, behind default-on net feature flag Doc: B) REQ-SHELL-3 Title: 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). Doc: PAIR 150: REQ-SHELL-3 <-> REQ-WORKER-LIST-VISIBILITY ================================================================================ A) REQ-SHELL-3 Title: 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). Doc: B) REQ-WORKER-LIST-VISIBILITY Title: V-1 (WORKER-TRUTH triage, operator rider): worker perches leave the DEFAULT `spt endpoint list` view — they are process-local machinery, not subnet citizens; leaked-or-live worker rows rendering as permanent OFFLINE endpoints is the operator-visible symptom root. A dedicated flag (--workers) reveals them (one command + flag per the --all/--detail precedent, NOT a separate list-working command — sister divergence deliberate). Applies to the human render, --json (additive default-absent filter), and the registry/projection legs; verify-and-stop any worker gossip into the subnet registry as peer endpoints. Doc: