# Known Hazards

Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.

**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.

---

## 1. Race conditions & ordering

### 1.1 Phantom INIT_SIGNOFF after grace period
- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.
- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.
- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.

### 1.2 Poll-rewrite race & info.json mid-write reads
- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).
- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.

### 1.3 Stale `index.lock` wedge from prior git crash
- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.

### 1.4 Deferred spool rows must not leak to the event stream
- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.
- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.
- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.
- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.

### 1.5 Worker (working-perch) lifecycle path consistency
- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.
- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.
- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.
- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.

---

## 2. Identity & session-binding

### 2.1 Parent PID over ephemeral poll PID
- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).
- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.
- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.
- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.

### 2.2 Stdin session_id precedence over env
- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.
- **Invariant:** stdin-provided session_id wins; env is fallback only.
- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.
- **Sister cite:** CHANGELOG v1.35.1 "IN-05"; `hook_subagent_start.rs:40-51`.

### 2.3 Binary-handoff argv schema must stay backward-compatible
- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.
- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.

### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.
- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes "same gen continuing" vs "new gen born".
- **spt-core mapping:** carries to the daemon's per-instance generation tracking.
- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/daemon_e2e/attach/brain_swap + `update.rs::apply_brain_only`, itself test-only-reached) — no non-test caller (grep-clean close-out). The falsifiable proof the production path needs no frame is the D4-2 hard-kill harness (`tests/resume.rs`): a brain dropped with no `snapshot()` taken still resumes gaplessly.
- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.

### 2.5 Daemon-hosted endpoints have no dedicated liveness PID
- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads "alive" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third category: the Psyche is neither harness-hosted (no `parent_pid` anchor) nor a broker-held PTY child.
- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT "Shell… Not in the subnet registry") and extends it to daemon-hosted *agent* perches.
- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.
- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.

### 2.6 A shell instance's `online` is a recorded field with no death edge
- **Failure:** a shell perch's `status` has exactly two writers — the bind handshake (→`online`) and `close_shell` (→`offline`) — and `close_shell` runs on the **link-break** path alone. A binary that dies **abruptly** (force-kill, crash, OOM) breaks no link, so nothing ever writes the flip: the record says `online` forever while the pid it names is gone. Every consumer of the recorded field then inherits the lie — `shell list` reports online indefinitely; `relink` (the one command that recovers the instance) refuses `SHELL_ALREADY_ONLINE`, gated on the very state that is wrong; and `shell cmd`'s wake-if-offline arm reads "not offline", skips the wake, and spools the command to a corpse — accepted happily, drained by nobody, **indistinguishable from a busy shell**. The only exit was `teardown` + `spawn`, which destroys the perch (losing the instance's persisted state) and frees the mint slot, so the instance returns under a **different canonical id** and every reference to the old name — docs, other agents' notes, tag addressing — silently points at nothing. The rename is the *visible* cost; the expensive one is **silent**: a consumer's perch-persisted cursor re-baselines on the fresh perch, so work arriving between the death and the re-bind is never scanned — not failed-and-retried, just never seen (alchemy's tag scanner, 2026-07-25). Recovery must therefore preserve the perch, not merely restore a running binary.
- **Invariant:** ONLINE-ness is **derived**, never the recorded field alone: `status == online` AND the recorded process identity is not *provably* gone. Missing/unparseable/zero PID or unavailable birth evidence fails toward alive; a positively mismatching native birth identifies a recycled PID as gone. Derivation never promotes a recorded-offline instance. Derive at relink, drive/cmd-wake, discovery and activity renders; preserve the recorded field at writers, owner-suspend cleanup and watcher eligibility. The latter adds the nonpersistent corpse-boot rule below rather than relying on stale status to suppress relaunch.
<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->
- **One process generation per read (releases#287):** modern `shell.launch.json` atomically stores `pid`, native `pid_started_at`, and wall-clock `launched_ms`. A modern `pid: 0` means explicitly pidless/cleared and never falls back to the numeric `shell.pid` mirror. Legacy records without `pid` retain their old numeric-PID plus optional-birth read. Publish the complete JSON identity before updating the compatibility mirror; failed publication preserves the previous identity. Never delete the previous birth before a fallible spawn: failure would leave a recycled PID looking unambiguously alive and could authorize its termination. Both launch arbitration and kill authorization consume the same coherent PID/birth snapshot; clearing retains launch age while retiring the process identity.
- **No spontaneous relaunch of a NONPERSISTENT same-boot death:** the releases#78 force-kill freeze still protects nonpersistent instances, so killing one to free its exe does not arm a watcher underneath an overwrite. Recovery remains demand-driven. **Persistent instances are exempt** (operator ruling, releases#287): a kill is not a request to keep a declared-persistent shell down; use `teardown` or a nonpersistent manifest instead.
- **Watcher eligibility:** recorded `offline`, no live binary awaiting bind, and either `persistent`, no corpse, or a corpse whose launch predates the machine boot instant. A nonpersistent corpse without a launch stamp or boot oracle remains frozen; persistent instances require neither to become eligible. Birth-safe probes prevent a recycled pid from masquerading as a live shell.
- **spt-core mapping:** `spt_store::shellinfo::{shell_pid_provably_dead, effective_status, is_shell_online}` (the resolver — the shell-side twin of 2.5's `liveness.rs`, which gave *agent* perches exactly this and which shells never got); consumers in `linkhost::{relink_shell, prepare_drive, run_action}`, `shelldisc::discover`, `activity::observe_links`; the class-(c) eligibility read is `spt_daemon::shellwake::watcher_eligible` (+ `launch_predates_boot`, `BOOT_RESTORE_SLACK_MS`), and the birth stamp it rests on is `shellinfo::{ShellLaunch, record_shell_launch, read_shell_launch}`.
- **Source:** spt-core, flynn's spt-alchemy field report (2026-07-25) — a deterministic recipe, not a race. Class-(c) re-based BAROMETER W2 (releases#78, doyle's ruling on comment 5156810267).

### 2.7 A daemon or machine restart strands `persistent` shells — and a graceful cascade fixture suppresses the failing arm `[REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS]`
- **Failure:** a restart can leave an `online` record over a corpse without a link-break or suspend edge. The original boot sweep fixed only machine reboots: it rejected corpses launched after machine boot, so a daemon-only restart still stranded every persistent shell. The measured releases#287 specimen launched at machine boot +46s and stayed down for 27 hours after a daemon restart despite its owner being online.
- **The owner-facing surfaces HIDE the fault**, which is why it survived unreported for days: every display path (`shell list`, its `--json` twin, the shell-context render) routes through the ONE discovery seam, which **derives** status (2.6) and so correctly reports the binary as offline — while the only consumer whose decision matters, the wake cascade, reads the RECORDED field that no owner-facing surface shows. Absence of a visible symptom is therefore **not** evidence of absence of this fault, and a clean-looking context render must never be taken as proof the class did not occur. The correct falsifier is the **on-disk record**.
- **The existing cascade test cannot fail on it — its SETUP SUPPRESSES THE FAILING ARM.** `rest_edges_cascade_shells_with_divergence` establishes its fixture by driving the suspend path, which itself writes the `offline` the code under test is supposed to encounter — so the restart shape (a record still saying `online` over a corpse, reached with **no** suspend edge) is unreachable from that fixture BY CONSTRUCTION. The test is not weak; it gives a true answer to the wrong question, so a reader asking "is the cascade covered?" is told yes. This is the third suppressed-arm instance found in one week, and the pattern is the same each time: **a fixture that establishes the precondition the code under test is supposed to establish.**
- **Invariant:** a daemon-only or machine restart must not strand a persistent instance. **(a) Heal:** every reconcile cycle writes `offline` only where the derived birth-safe read contradicts recorded `online`; already-correct records are untouched. **(b) Restore:** the once-per-daemon-generation boot sweep and every owner offline→online edge run the same per-owner body. Relaunch every instance whose adapter declares `persistent`, whose owner is online, and whose binary is down in fact. There is **no machine-boot, launch-age, or stamp-presence eligibility gate**. A live binary awaiting bind is not down and must not be duplicated; missing birth evidence keeps the existing fail-toward-alive liveness behavior. The owner-online trigger remains an edge, bounding failed launches to one attempt per event.
- **Regression shape:** construct recorded `online` over a corpse **without a suspend edge**, including a launch **after machine boot**, then bring its owner online after a spent boot sweep. Every stranded persistent sibling must be restored; offline owners and nonpersistent instances are not restored. Separately defend live/binding exclusion and the nonpersistent watcher freeze. Suspending in setup suppresses the failing arm and proves nothing about this class.
- **Per-owner enumeration:** the existing `SHELL_RECORD_HEALED:<owner>/<shell_id>` daemon diagnostic names **every** successfully healed instance, not one representative. Healthy and already-healed records produce no event. The restore diagnostics likewise identify each attempted instance and distinguish boot from owner-online triggers. <!-- [doc->REQ-SHELL-HEAL-ENUMERATION] -->
- **spt-core mapping:** `spt_daemon::shellwake::{heal_stale_online_records, restore_persistent_shells_at_boot, restore_persistent_shells_on_owner_online, OwnerOnlineEdge}`; `shellhost::live_launch_winner`; `spt_store::shellinfo::{ShellLaunch, shell_pid_provably_dead}`. `launch_predates_boot` and `BOOT_RESTORE_SLACK_MS` now govern only nonpersistent watcher eligibility.
- **Source:** BAROMETER W2 (releases#78); releases#287 operator ruling (2026-09-08), superseding the persistent force-kill freeze and boot-instant conjunct.
<!-- [doc->REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS] -->

---

## 3. Lifecycle

### 3.1 Ephemeral perch cleanup on every `ring` exit path
- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.
- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.
- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.
- **Sister cite:** `src/owl/ring.rs:58-294`.

### 3.2 Stale signoff sentinel must not kill a fresh start
- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.
- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.
- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.
- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.

### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.
- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.
- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.
- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.

### 3.4 A `ring` never adopts — so never deletes — a perch it did not create
- **Failure:** 3.1 guards the LEAK direction (cleanup on every exit path). It says nothing about *whose* perch is being cleaned, and the two compose into a data-loss bug: `ring` decides "the caller has no perch" **ready-marker-first**, so an existing perch whose marker is momentarily down — busy turn, soft session-end, stale re-bind — reads *perchless*, `setup_ephemeral` clobbers the caller's real `info.json`, and 3.1's guaranteed cleanup then runs on the way out and **deletes** the ready marker, `info.json`, `spool.db` and the directory. Silent CLI-side fs ops, no daemon log, no console trace (field 2026-07-27 — emphasys lost a live endpoint and its spool to one timed-out ring).
- **The mail half is a CONFIDENTIALITY failure, not only a durability one:** deletion is what happens on the way out, but while the ring *holds* the adopted perch its reply-wait `drain_one_at` consumes whatever is in the victim's spool and **renders it to the ringer as the reply**. Ringer == victim (the field case) is the degenerate, mild shape — anything drained reaches its intended recipient. Ringer != victim is the severe one: a third party rings a live agent and the **victim's inbox is drained into the ringer's output**, leaving no trace on either side once the dir is removed. Across a multi-machine subnet that is one agent reading another's mail. Bounded honestly for the record: in the field instance theft did **not** fire — emphasys's 66-second window returned `TIMEOUT` with zero `Replied{…}` — it was available and did not happen.
- **Self-camouflaging**, which is why it went unattributed for hours: the damage — perch gone, roster row ghosted — *mimics the stale-liveness condition people reach for `ring` to diagnose*. The field caller was probing a ghost-roster symptom and the probe manufactured a fresh one, so debuggers of stale liveness are disproportionately its victims and read the wreckage as more of the symptom they were chasing.
- **Marker-down is not an edge case:** on hosted OMP endpoints the extension owns the listener and publishes busy/idle through `api state`, so marker-down is the **normal steady state during work** — marker-first misjudges healthy endpoints routinely.
- **Invariant:** probe the perch **directory**, not the marker. A dir carrying a **record or a spool** is OCCUPIED — refuse to adopt it. Ambiguity resolves toward refusal, always: an **unreadable** record (corrupt/truncated `info.json`) or an unreadable dir counts as occupied, never as residue, because deletion is irreversible while refusal is recoverable. A pre-existing **empty** dir is refused too — it is not provably ring's own residue (a bringup mid-create owns an empty perch dir for a window, and deleting it is the same race in a different hat). Refusal never blocks and is never silent: the message is delivered, the call declines to block-wait (the reply lands on the caller's own listener), and the caller gets a **distinct** loud report — `RING_PERCH_EXISTS` (record/spool) or `RING_STALE_DIR` (empty; names the path and the manual remedy). Loudness — not a self-heal — is what keeps a permanent refusal from being a silent one. Structural, not advisory: the leaf dir is created with `create_dir` (fails `AlreadyExists`), so a perch appearing between probe and create still cannot be adopted, and the refusal path never calls cleanup.
- **spt-core mapping:** `spt_msg::ring::{probe_perch, setup_ephemeral, ring}`; `RingOutcome::{PerchExists, StaleDir}` rendered by `cmd_ring`.
- **Source:** spt-core field incident 2026-07-27 (emphasys endpoint + spool loss); ruled by doyle, with emphasys's mid-create-race amendment revoking the empty-dir self-heal.
- **Amendment — the perch GC inherits this reasoning EXTENDED, never carved out (doyle, 2026-08-04, releases#109).** A second consumer now deletes perch directories: `spt endpoint gc` (3.5, `REQ-PERCH-GC-RESIDUE-PREDICATE`). Its authority is something `ring` never had — a **positive** statement that the directory carries **no endpoint record at all**, from the store that answers endpoint existence on a node (the owlery tree itself; the perch dir plus its parseable `info.json` IS the endpoint record). That authority does **not** outrank this section's occupancy rules, and the ruling says why: undeliverable-by-construction settles *deliverability*, not *value*, so a recordless dir carrying a **spool** is still refused — that spool is the only surviving copy of whatever was queued, and deleting mail is not a GC's call at any confidence level. A pre-existing **empty** dir is still refused for the reason emphasys's amendment gave: the mid-create window exists for the GC identically, and no still-positive discriminator for "not mid-create" exists for an empty dir (there is no create-intent marker, and mtime is out by the GC's own predicate). What the GC *may* delete is therefore strictly narrower than what it may *call* residue, and both refusals are kept honest exactly as this section requires — **loudly**, per path, under distinct tokens (`RESIDUE-WITH-SPOOL` / `RESIDUE-EMPTY`) with the manual remedy named. A refusal also **shields the whole subtree under it** — a nested child that would qualify on its own is reported `SHIELDED-BY-REFUSED-PARENT` and left alone, because refusing a directory has to mean not modifying its CONTENTS either (carving a child out destroys part of what the refusal report asked the operator to look at, and can move the parent's own verdict on the next pass). The one discriminator the GC has and `ring` does not is the **held `.info.lock`**: a NON-empty recordless dir whose per-perch write sentinel is held right now is a bringup mid-create, provably, and is refused (`OCCUPIED-WRITE-IN-FLIGHT`). An empty dir gets no such signal, which is why its refusal stays unconditional.
<!-- [doc->REQ-HAZARD-RING-PERCH-ADOPTION] -->

### 3.5 `is_perch_alive` is INVERTED on perch residue — a GC must never key on it
<!-- [doc->REQ-HAZARD-PERCH-GC-LIVENESS-INVERSION] -->
<!-- [doc->REQ-PERCH-GC-RESIDUE-PREDICATE] -->
- **Failure:** the obvious way to write a perch garbage collector is "reap what is not alive" — `!spt_store::liveness::is_perch_alive(dir)`. On the population a GC actually walks, that predicate is **backwards**. `is_perch_alive` returns **true** for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity — an absent record means a wrapper-owned listener between polls), and **false** for a daemon-hosted endpoint whose `status` is `offline`. Measured on HFENDULEAM 2026-08-04 across 38 top-level perch dirs: **all 24 recordless residue directories read ALIVE, and all 6 offline REAL endpoints read DEAD.** A sweep keyed on `!is_perch_alive` would have spared every stray probe directory and deleted six resting agents' records — it does not under-perform, it authorizes **exactly the inverse set**.
- **Why it is a hazard and not a bug in the resolver:** `is_perch_alive` is correct for its own job. Fail-toward-alive is the safe direction for DELIVERY (2.5) — the cost of a false "alive" is one message taking the spool path. It is the wrong direction for DELETION, where a false "not alive" is irreversible. The inversion is what happens when a predicate is reused across a polarity boundary it was never sized for.
- **Invariant:** perch garbage collection classifies on record **presence** only (3.5's predicate: no `info.json` present on any read attempt ⇒ residue; NotFound kept distinct from every other I/O error, because absent is the only answer that authorizes deletion) and **never calls a liveness resolver** — not `is_perch_alive`, not `is_registry_entry_alive`, not a pid probe. The registry is likewise never asked: `clean_stale_entries` deletes dead-pid rows and `unregister_address` fires on an ordinary stop, so registry-absence is the normal steady state of every offline endpoint (4.3). The unit pinning this asserts **both** arms with the shipped resolver as the witness: residue reads alive and is reaped anyway; an offline endpoint reads dead and is kept anyway.
- **spt-core mapping:** `spt_store::perchgc::{sweep, PerchClass}`; `spt endpoint gc` renders it. The refusal/report contract is `REQ-PERCH-GC-REFUSAL-REPORT`; the occupancy rules it inherits are 3.4.
- **Source:** measured by todlando on HFENDULEAM 2026-08-04 while answering doyle's step-1 predicate question on releases#109; ruled into the lane by doyle the same day.

---

## 4. Wire / transport

### 4.1 Envelope HTML-entity codec ordering — `&amp;` decoded LAST
- **Failure:** decoding `&amp;`-entity before the others double-decodes nested entities (`&amp;amp;lt;` → wrong result).
- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`&lt;`,`&gt;`,`&quot;`, then `&amp;`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.
- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.
- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.
- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\r` too — `event_body_escape` folds CRLF/lone-CR to `\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\r` was never line-representable here, so normalizing it is robustness, not an ADR-0001 wire divergence (decoder + amp-last untouched). Belt-and-suspenders: `spt send`/`ring` trim stdin like `notify`.

### 4.2 Two-slice envelope parser is panic-free and tolerant
- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.
- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.
- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.
- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.

### 4.3 Registry stale-entry cleanup precedes lookup
- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.
- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.
- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.
- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.

### 4.4 Deferred rows survive poll drain
- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.
- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.
- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)

### 4.5 Inbox legacy compat must not double-deliver
- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.
- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.
- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve "never read at drain time."
- **Sister cite:** `src/common/inbox.rs`.

### 4.6 Addressable-id charset reserves the address delimiters
<!-- [doc->REQ-HAZARD-ID-CHARSET] -->
- **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`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.
- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.

### 4.7 Concurrent SQLite openers must not fail with "database is locked"
<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->
- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / "database is locked" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.
- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).
- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.

### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)
<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->
- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or "last write wins"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.
- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).
- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.

### 4.9 SQLite stores must create their parent dir — SQLite won't
<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->
- **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, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.
- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)
- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.

### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`
<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->
- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the "node" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… after its key universe flipped during the sudo experiments); the dead identity's `sergey` row made bare `spt send sergey` refuse on HFENDULEAM.
- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhile resolution honestly reports it absent instead of poisoning bare-id sends.
- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Ghost registry row eviction"); the AMBIGUOUS render fix rode along.
- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. "No transitive gossip" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So "any future update for a node comes from that node itself, alive" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)

### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`
<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->
- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.
- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).
- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).
- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.
- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.

---

## 5. Platform-specific

### 5.1 Windows PID recycling false positives
- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.
- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.
- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.
- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.

### 5.2 Windows EBUSY on atomic rename
- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.
- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.
- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.
- **Sister cite:** CHANGELOG "EBUSY"; `src/common/owlery.rs` atomic_write.

### 5.3 Git/subprocess timeout stamping
- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.
- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.
- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.
- **Sister cite:** `src/common/git.rs`.

### 5.4 Windows UNC prefix in serialized paths
- **Failure:** canonicalized `\\?\C:\...` serializes to `//?/C:/...` and fails `read_to_string`.
- **Invariant:** strip the `\\?\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.
- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
- **Sister cite:** `src/common/owlery.rs:377-384`.

### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.
- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.
- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).
- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.

<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->
### 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::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for hours.
- **Invariant:** every detach-spawn of a long-lived child inherits only handles it **enumerates**. The default and overwhelmingly common form of that is `bInheritHandles = FALSE` (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every **bare** `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).
- **The one permitted TRUE (amended 2026-07-26, RESIDENT-SERVICE W1):** a spawn that must capture the child's output may pass `bInheritHandles = TRUE` **only** paired with an explicit `STARTUPINFOEX` + `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` allowlist — the Win32 contract honors the list only under TRUE, so the earlier phrasing ("flipping TRUE is refused") forbade its own mechanism and was unimplementable. The hazard was always *unenumerated* inheritance, and the list **is** the enumeration: only the named handles flow, so a grandparent's pipe cannot reach the child at any depth — strictly stronger than what FALSE achieves incidentally. A **bare** TRUE, with no attribute list, remains REFUSED. Today the only such path is a supervised `[service]` spawn with startup capture (one-entry list, one file handle), and it is pinned behaviorally, not by flag-reading: `a_captured_spawn_inherits_only_the_handle_in_its_allowlist` plants a deliberately inheritable handle that is NOT in the list and fails if the child receives it.
- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn, or the enumerated form above where it genuinely must capture output.
- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.

### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`
<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->
- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads "not running", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost rows).
- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs as-is with a loud warning — a consistent universe, never a torn one. Elevated one-shot *clients* talking to an unelevated daemon are fine (downward connects work); the daemon side is the invariant.
- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".

<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->
### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`
- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).
- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).
- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 "flag combo breaks spawn" regression class); window-absence was verified live by process-watch capture.
- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.
- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).

### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`
- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean "everything due now"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.
- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent "never run / due now" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.
- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.
- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.
- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.

### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`
- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to "run as administrator / root". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.
- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.
- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).
- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.
- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.
- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.

### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`
- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c "… $id …"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.
- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout across the privilege boundary — the child is self-contained (on Windows it self-pauses a fresh console via `GetConsoleProcessList` so its output stays legible). The print-hint floor prints the absolute-path command too.
- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).
- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).
- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.

<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->
### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`
- **Failure:** `portable-pty`'s ConPTY spawn resolves a bare program name with a `which` that takes the FIRST `PATH` match. A node/npm CLI installs as BOTH an extensionless shebang shim (`ccs`, for Git Bash) and a Windows launcher (`ccs.cmd`) in the same dir; portable-pty picks the extensionless `ccs`, and `CreateProcessW` then tries to execute that non-PE file and fails with **os error 193** ("%1 is not a valid Win32 application"). Live failure: `spt endpoint run claude-spt:ccs` → `CreateProcessW C:\nvm4w\nodejs\ccs` 193 (operator, 2026-06-16). The same bites any harness/shell whose `[session.self]`/`[shell].spawn` names a `.cmd`/`.bat`/`.ps1`-backed command — `CreateProcessW` cannot execute a batch or PowerShell script directly.
- **Invariant:** spt-term resolves the program ITSELF before handing it to `CommandBuilder`, bypassing portable-pty's `which`. A bare name is searched over `PATH` × `PATHEXT` (whose default order already prefers `.EXE`/`.COM` over `.BAT`/`.CMD`), then an extensionless fallback. A non-PE target is wrapped in its interpreter: `.cmd`/`.bat` → `cmd.exe /d /c <path>`, `.ps1` → `powershell -NoProfile -File <path>` (the wrap args precede the caller's args); a real executable spawns directly; an unresolvable name passes through unchanged (never makes a working case worse). Unix is a passthrough — `execve` honours a shebang on an extensionless script. Applied at the ONE `CommandBuilder` chokepoint (`PtySession::spawn_program_in`), so every broker harness + shell spawn is covered. *Caveat:* the `cmd.exe /d /c` wrap inherits cmd's argument-quoting rules for paths/args containing spaces or cmd metacharacters — adequate for the common install-path case; a fully robust cmd-quoting pass is a follow-on if it bites.
- **spt-core mapping:** `spt_term::winprog::{resolve_for_pty, resolve_in}` (the pure PATHEXT-precedence kernel + the Windows env wiring), wired into `spt_term::pty::PtySession::spawn_program_in`. Unit: `resolve_in` precedence (`.cmd`-over-shim, `.exe`-direct, explicit-extension, path-order, passthrough) [`winprog.rs`].
- **Source:** field diagnosis 2026-06-16 (operator dogfood, `claude-spt:ccs` bringup) — doyle.

<!-- [doc->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] -->
### 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 nested psyche record 125 bytes all-NUL); `Get-Content` renders blank, `Format-Hex` shows the zero-fill. A wiped-but-present record then read as a live ONLINE endpoint for days (compounded by 5.14).
- **Invariant:** for records whose loss is **unrecoverable**, flush data to stable storage **before** the rename publishes the name — `File::create(tmp)` → `write_all` → `sync_all()` → `rename_with_retry`. Then a crash yields the complete old file OR the complete new file, never a NUL husk. (A file of correct length that is ALL-NUL is the diagnostic tell of this class — a non-fsync'd write caught by a hard reset.) **Durability is SCOPED, not blanket.** `fsync`ing *every* atomic write serializes a durable flush behind each of the ~26 `atomic_write` callers — and for `info.json` the flush lands under the per-perch `.info.lock` (the **W1b lock-across-fsync wedge shape**, §6-era) — which stalls daemon bringup 4–6× so the endpoint misses its ONLINE window. Scope the fsync to the record, not the writer: only authoritative per-perch records and one-shot identity material are durable; reconstructible state (registry snapshots, epoch counters, peer caches, the release cache) stays non-durable — a post-crash NUL husk parse-fails to *absent* and is regenerated / re-gossiped, and the 5.14 read-side already makes a corrupt record harmless at every seam.
- **spt-core mapping:** `spt_store::atomic::atomic_write_bytes_durable` / `atomic_write_string_durable` are the opt-in durable siblings; the default `atomic_write_bytes` / `atomic_write_string` stay non-durable. Durable callers are exactly `spt_store::info::write_info` (the perch record — the hall-a surface), `spt_store::nodeid` (the node seed — corrupt is node-bricking, never regenerated), and `spt_daemon::machineid` (minted-once machine id — a NUL husk would silently re-mint a different id). Reader-side handling of an already-corrupt record is the sibling 5.14. **Canary:** `attach_wedge_e2e` is the regression guard — a blanket fsync fails it (bringup misses the 20s ONLINE budget); keep durability scoped so it stays green.
- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; blanket-fsync perf regression pinned + scoped by the gate rig (doyle), same date.

<!-- [doc->REQ-HAZARD-CORRUPT-PERCH-COHERENCE] -->
### 5.14 Corrupt info.json read as ABSENT → fail-open readers gossip a wiped perch ONLINE  `[REQ-HAZARD-CORRUPT-PERCH-COHERENCE]`
- **Failure:** three readers each collapsed a **corrupt** (present-but-unparseable) `info.json` into their fail-open ABSENT default, so a NUL-wiped perch (5.13) read as permanently live: `is_perch_alive` returned `true` (unreadable ⇒ interim-alive), `advertised_status` then saw alive + no resting record ⇒ `Active`, and the daemon self-gossiped that Active row every round (epoch 173k+). Result: `hall-a`, dead since a machine restart, showed ONLINE in `spt whoami` and on every remote picker.
- **Invariant:** CORRUPT ≠ ABSENT. A record that EXISTS but stays unparseable across the retry budget is a **destroyed** record — never a live endpoint. `is_perch_alive` reads corrupt ⇒ **not alive** (ABSENT keeps interim-alive parity — the ONE case that stays true); `advertised_status` then lands corrupt in the cold arm ⇒ **Suspended**, never Active/Dormant. Readers that decide liveness/status must branch on the tri-state (present / absent / corrupt), not an `Option` that fuses the last two.
- **spt-core mapping:** `spt_store::liveness::{read_raw_state, is_perch_alive}` (the tri-state kernel) and `spt_daemon::registryhost::advertised_status` (cascades to Suspended off the alive fix). `list_self_perch_ids` deliberately still lists a corrupt dir by existence — that is the *visibility* the local-roster fix (counter-39 #3) relies on, not a liveness claim. Candidate sibling seam `is_registry_entry_alive` (corrupt hosted row falls to a daemon-pid probe) is parked — it does not produce the ONLINE gossip.
- **Source:** field diagnosis 2026-07-01 (operator, post-v0.19.0 restart) — counter-39 bug #2; sequel to the v0.17.0 W4 presence-truth fix (cold ⇒ Suspended) which a corrupt perch bypassed via false-alive.

<!-- [doc->REQ-HAZARD-ATOMIC-TMP-COLLISION] -->
### 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: `establish_perch` returns `BindError::Io` *after* having written the record, mock-session's bind-is-fatal check `exit(1)`s, the harness dies, and the endpoint never reaches ONLINE. A microsecond window that existed forever (bare `fs::write` + rename back-to-back) — the 5.13 `sync_all` widened create→rename ~50× (to ~4ms) and made the collision near-certain *exactly at bind*, which is why the durable-perch-record change unmasked it (deterministic wedge, not a perf regression).
- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind to them.
- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.
- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.

<!-- [doc->REQ-HAZARD-INFO-RMW-LOST-UPDATE] -->
### 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. The surviving `info.json` is a **pre-bind shape carrying `status=online`** — bringup passes (it sees ONLINE), the broker serves the session, then on child death `reconcile_hosted_liveness` reads `state != "live_agent"` (or `controllable != Some(true)`) and *silently `continue`s* — the dead endpoint is latched ONLINE forever. This resurrects counter-39 #2's dead-but-ONLINE field surface through a brand-new path. Once-loud, now-silent: 5.15's unique tmp turned the previously-`os-error-2` collision into a clean last-writer-wins, and the 5.13 durable fsync (inside the RMW read→write window) made the interleave near-certain on a normal-speed disk. The race predates all of this (v0.19.0-era) — the old fixed tmp made it fail LOUD and non-durable kept the window µs-narrow.
- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interleaves a stamp. Readers stay lock-free (the atomic rename already gives a complete old-or-new record). Holding `.info.lock` across the durable fsync is ms-scale on these infrequent record writes (the same thing every `mutate_info` already does), NOT the per-keystroke W1b lock-across-fsync shape.
- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field were converted to the `mutate_info` CAS: `spt::api::reporting::cmd_boundary` (rc-rebind session-id rotation) and `spt_store::home::adopt_for_unset` (home-subnet adoption; the `is_none()` re-check runs inside the closure). **Deliberately parked (known unlocked-RMW residue, honest coverage):** `spt_msg::listener::write_busy` (BUSY-pid stamp — self-healing: re-marked next delivery, recency next tick) and `spt_store::rename::rewrite_id` (rare operator rename ripple) still do a raw read→write; a lost update there self-heals or is operationally rare, so they are left as follow-ups. `spt_msg::ready::start_homed` (harness-hosted `ready`/`listen` bind) is the same read→build→write *shape* as `establish_perch` but is a single-writer context (the daemon does not host/stamp a harness-hosted ready agent concurrently at its own bind), so it is left as-is — revisit if a daemon-hosted path ever stamps it. Regression guard: `write_info_racing_mutate_info_never_lost_update` — a `Barrier`-aligned RMW-stamp vs full-bind-record write hammer on one perch asserting the full record's fields never vanish under a racing stamp (reds on the unlocked-write code, greens after the lock).
- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.

### 5.17 `powershell` never executes under our detached spawn — a silent no-op that costs every spawn-rig builder the same three runs
- **Failure:** a test rig that spawns `powershell -Command …` through `daemon::detached_no_inherit*` (`DETACHED_PROCESS`, no console, no inherited handles) gets a process that **starts and exits having run nothing** — not even a bare `Set-Content`. No error, no output, exit status unremarkable: the rig reads as "my code did not do the thing" when in fact the *fixture* never ran. Measured 2026-07-26 while building the `[service]` supervision gates: three consecutive runs burned on a fixture that could not have worked, each one re-diagnosed as a supervisor bug. `cmd /c` and `sh -c` run fine under the identical spawn, so the no-op is specific to the shell, not to the detachment.
- **Invariant (rig discipline, not product code):** spawn-based fixtures use `cmd` on Windows and `sh` on unix — never `powershell`. And find a spawned process's descendants **by parentage** via `spt_store::proc::process_table()` (which returns `(pid, ppid)`), never by asking the child to report them: a grandchild that must cooperate to be found is a rig that cannot observe an *uncooperative* one, which is the case the gate exists for.
- **spt-core mapping:** every spawn rig under `crates/spt-daemon` — `servicehost.rs` (`long_running`/`instant_exit`/`noisy_instant_exit` fixtures, the tree-teardown gate) and any future rig over `daemon::detached_no_inherit_env`. No `REQ-HAZARD-*` id and no regression test **deliberately**: the invariant constrains how tests are written, and the only assertion available would be "the OS still refuses to run powershell this way" — an instrument test, which is never requirement evidence. It is numbered here because the cost is paid by whoever writes the *next* rig, and a fact that reads as a bug in your own code is exactly what this document exists to intercept.
- **Source:** RESIDENT-SERVICE W1 leg D (todlando, 2026-07-26); ruled into this document by doyle the same day.
- **Amendment 2026-08-02 (hertz, releases#110 measurement):** the cause is `DETACHED_PROCESS` **alone** — not the no-inherit half, as the entry's parenthetical implied. Measured on HFENDULEAM with raw `creationflags` and fully piped stdio: `powershell -Command "Set-Content …"` under `DETACHED_PROCESS` exits **rc=0 having written no file** (it does not execute); under `CREATE_NO_WINDOW` the identical spawn writes the file and returns its stdout. `cmd /c` behaves DIFFERENTLY from powershell and the entry's "runs fine" is half-right: `cmd /c echo ran> file` under `DETACHED_PROCESS` **does execute** (file written, rc=0), but a `cmd /c ping` whose stdout is a pipe returns **zero bytes** — cmd runs and its piped stdout is lost. So the rig rule stands, with the sharper statement of it: under `DETACHED_PROCESS`, powershell does not run at all and cmd's **captured output** cannot be trusted. A rig that parses a detached shell's stdout is reading a value that no longer exists — see 5.18 for the concrete false-green this shape would have manufactured.

<!-- [doc->REQ-HAZARD-TREE-CWD-PIN] -->
### 5.18 A surviving test descendant cwd-pins the tree it ran in — the `.worktrees` skeleton that will not delete  `[REQ-HAZARD-TREE-CWD-PIN]`
- **Failure:** a directory that is some live process's **current directory** cannot be renamed or deleted on Windows, while files *inside* it stay writable. That asymmetry is why the symptom never reads as a file lock: `git worktree remove` fails naming the **directory**, `openfiles` shows nothing, and the tree looks idle. cargo runs each test binary with cwd = the **package root**, so every child a test spawns inherits `<worktree>/crates/<pkg>` as its own cwd — and any child (or **grandchild**) that outlives the test pins that path forever. Result: a silent `.worktrees` skeleton accumulator, each one costing the next agent an afternoon.
- **Mechanism — the common case is a surviving descendant, and it is not the `CREATE_NO_WINDOW` conhost.** releases#110 filed the holder as an *orphaned `conhost.exe` holding the crate root*; measurement on HFENDULEAM 2026-08-02 **refutes** that for the population the rigs mint. `CREATE_NO_WINDOW` does produce exactly **one conhost per console child** (counted by `ParentProcessId`), but that conhost's cwd is **not** inherited: it is launched with a system cwd. Census: **43 readable, all of them `C:\WINDOWS\` or `C:\WINDOWS\system32\`, zero under any worktree** (4 unopenable, stated). Held directly: spawning a child with cwd = the package root and then force-killing it leaves the orphan conhost still reading `C:\WINDOWS\`. Positive control on a scratch dir — write-inside **OK**, delete **BLOCKED**, kill the **conhost** → still BLOCKED, kill the actual holder → **deletes first try**. The holder in that control turned out to be an orphaned **grandchild** (`PING.exe`, cwd inherited through `cmd`) that survived its parent's kill. **That conhost is a byproduct, never the pin; killing it is not the cure and "no conhosts" is not the all-clear.**
- **Amendment 2026-08-02 — the exception, field-refuted before it shipped: a `--headless` ConPTY conhost DOES inherit its creator's cwd, and pins it alone.** The entry previously generalized the census above into "a conhost's cwd is never inherited". That is true of the `CREATE_NO_WINDOW` population and false of the ConPTY one, which no rig mints but every terminal-hosting path does.
  - **Field specimen (BAROMETER W4 teardown):** `pid 46556 conhost.exe --headless --width 80 --height 24 --signal 0x214 --server 0x20c`, born during the build, parent dead, PEB cwd `…\.worktrees\barometer-w4\crates\spt\` — the **sole** holder. Killing it deleted the tree that had refused to delete.
  - **Reproduced deliberately, both launch paths.** Minted through `CreatePseudoConsole` from a process whose cwd was a scratch dir: the new `--headless` conhost's PEB reads **that dir**, and with it alive — write-inside **OK**, delete **BLOCKED**, then **DELETED** after `ClosePseudoConsole`, nothing else changed. Launched as `conhost.exe --headless` directly with a working directory: same inheritance. The first covers the path the wild specimen took; the second shows the binary takes its launcher's cwd in that mode regardless.
  - **The pty-teardown alternative is excluded causally, not by absence.** "Zero children" clears nothing here — a ConPTY client is a child of whoever called `CreatePseudoConsole`, never of the conhost — so killing the conhost could in principle have taken a living client with it. Kill-**order** rig, client deliberately `cd`'d elsewhere so it cannot be the holder: both alive → **BLOCKED**; kill the **client** only → still **BLOCKED**; kill the **conhost** → **deleted**.
  - **A census of live `--headless` conhosts cannot settle this in either direction.** All 8 alive that day read `C:\WINDOWS\system32\` — and their single creator's cwd *is* `C:\WINDOWS\system32\`, so the value inheritance predicts equals the value OS-assignment predicts, on rows uniform in the asserted property. It reads as confirmation of the old claim and carries no information; the deliberate launch is what discriminates.
  - **Elevation is not the discriminator, and neither reader needs it.** Both readers open with `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` only and, **unelevated**, read ~46–49 of the ~50 conhosts alive. A first pass that reported *no* conhost readable was a parsing fault in the caller, not an access denial — worth stating because "not readable" and "read as `C:\WINDOWS\`" are the same blank row to a careless reader.
  - **Scoped conclusion:** a `CREATE_NO_WINDOW` conhost is never the pin and killing it is never the cure; a `--headless` ConPTY conhost **can be the sole pin**, and there killing it **is** the cure. Neither sentence licenses the other.
- **Triage step — the finder answers WHO pins, never whether it is SAFE TO KILL.** These are different questions and the blocked-delete error is **identical** for both, which is exactly what makes the step worth writing down. Read the holder's identity *before* acting: an **orphaned descendant** (parent dead, no window, no client) → kill it; a **LIVE process — above all an agent's own shell** → `cd` out, never kill. **When the holder is a `conhost.exe`, its COMMAND LINE is the discriminator** — a bare `conhost.exe 0x…` is the `CREATE_NO_WINDOW` byproduct and is never the pin (look behind it for the descendant), while `conhost.exe --headless --width … --server …` is a ConPTY host that can be the pin itself and dies to a kill. Do not read its **child count** as evidence either way: a ConPTY's client is a child of the pseudoconsole's creator, not of the conhost, so "zero children" says nothing about what is alive behind it. Live confirmation 2026-08-02: after one worktree cleared, an identical error on a second traced to a working agent's **own pwsh tool shell**, and behind it that agent's **Bash tool** cwd in the same tree (the two hold *separate* cwds, so clearing one leaves the other). Pattern-matching the kill recipe there would have cost a working agent their session. The pin also follows whatever crate the test ran under, not a fixed package.
- **spt-core mapping:** `.github/ci/find-cwd-holders.ps1` — read-only (`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`, `NtQueryInformationProcess` → PEB → `RTL_USER_PROCESS_PARAMETERS.CurrentDirectory`), no process opened for write or killed; `-Match <substring>` filters by cwd. It reports its **denominator** (readable vs not-openable) because "0 rows" from a filter and "0 rows" because nothing could be opened are the same output and very different facts — an empty result means "no pin I could see", never "no pin". `handle.exe` is not on the box by default and does not distinguish a cwd pin from an open handle; `openfiles` cannot see this at all without the reboot-time maintain-objects-list flag. The **prevention** side is teardown completeness — a rig that reaps its own descendants leaves nothing to pin — which is the `REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN` (#99) class extended to grandchildren, measured by surviving-process count on both arms rather than asserted.
- **Do NOT "fix" this with the spawn flag.** Flipping the rigs' `CommandNoWindowExt::no_window()` (`crates/spt/tests/common/mod.rs`) from `CREATE_NO_WINDOW` to `DETACHED_PROCESS` removes the conhost (measured: 1 → 0) but does **not** remove the pin (that conhost is never the holder — the `--headless` ConPTY exception above is a host no rig mints through this helper), and it breaks rigs outright per the 5.17 amendment: `job_escape_e2e::conhost_children_of` shells out to **powershell** through that very helper and falls back to `unwrap_or(0)`, so a detached spawn would make `wmi_daemon_run_has_no_conhost_window` — the guard for `REQ-HAZARD-WMI-DAEMON-WINDOW` — assert "no conhost" **vacuously green**, a gate whose failure signature is identical to the fault it guards. Nor can the helper simply hand children a neutral cwd: `bind_cwd_project_e2e` and `resume_template_e2e` deliberately assert on the **inherited** cwd, which is the behavior under test.
- **Source:** hertz, releases#110 — filed diagnosis post-v0.51.0 (2026-08-01); mechanism re-measured and the conhost attribution **refuted** 2026-08-02 (per-parent conhost counts, box-wide conhost cwd census, orphan-grandchild positive control). doyle's live correction the same day supplied the live-shell half of the triage step. **The `--headless` exception is field evidence from todlando** (BAROMETER W4 teardown specimen, the direct-launch inheritance test, and the kill-order rig that excluded pty teardown), reproduced through the `CreatePseudoConsole` path with a delete-blocked/deleted control by hertz, and ruled an in-milestone amend by doyle the same day — the entry was refuted **before** it shipped in a golden run.

<!-- [doc->REQ-TEST-ENV-ONE-LOCK] -->
### 5.19 Two mutexes over one process-global env var guard nothing between them  `[REQ-TEST-ENV-ONE-LOCK]`
- **Failure:** two test-helper crates each declare their own `static ENV_LOCK` over the SAME process-global variable (`SPT_HOME`). Each serializes its own callers **flawlessly** — which is exactly what makes the arrangement read as correct — and neither serializes helper A against helper B. Whichever writes last wins. Because `spt_store::perch::spt_home()` is **not memoized** (it re-reads the env on every call), a test writes its fixture into home A and reads it back from home B: the store is absent, an empty access baseline is minted over it, and the assertion fails on a policy that was never loaded. Field shape (releases#111): `cargo test -p spt-daemon --lib dispatch::` lost **1–3 of 26 nondeterministically**, `an_unadmitted_asker_is_refused_at_this_nodes_own_seam` reading `Some(offline)` where the rules should have answered `None`.
- **Invariant (rig discipline, like 5.17):** one variable, one lock, **per binary**. A crate may keep its own ergonomic wrapper (`with_home`, `isolated_home`, a `TestHome` guard) but must **never** keep its own mutex over a variable another helper in the same binary also writes. `spt_test_support::env_lock()` is THE lease; crate-local helpers return that guard. It is **not reentrant** — a `TestHome` holds it for its whole lifetime, so one is never constructed inside a body already holding the lease.
- **Why it hides, and where it does not:** invisible under **nextest** (per-test processes) and invisible when a loser is run **alone** (no concurrent holder), so CI's nextest unit lane is structurally blind to it — the cost falls entirely on whoever hand-runs the module during a gate. This is the same "invisible-by-construction" property as 5.17, and the reason it is numbered here rather than left to the requirement.
- **The diagnostic that shortcuts it:** the tell is a **path shape the suspected helper could not have produced**. #111's panic printed `<tmp>\home\identity\trust\access.json` — a `home` SUBDIR, which `with_home` never makes (it points `SPT_HOME` at the tempdir *root*). That single mismatch named the real aggressor (`spt_test_support::TestHome`) faster than reading either helper. Corollary for the filed-mechanism trap: `with_home` holds its mutex across its **entire body**, so "a sibling `with_home` test retargets the home mid-test" — the filed story — was impossible; read a helper's lock SCOPE before accepting a race that names it.
- **spt-core mapping:** `spt_test_support::env_lock` (the one lease) consumed by `spt_daemon::test_home::{env_lock, with_home}` and `spt::testutil::isolated_home`. Guard: `test_home::tests::with_home_and_testhome_serialize_on_the_same_lease` — a `with_home` body holds the lease while a second thread attempts a `TestHome`, asserting the thread cannot enter and `SPT_HOME` cannot move, **then** that it does proceed once released (so a shared lock is proven to be a lock, not a deadlock). Reds on the two-lock code, greens on one. **Sweep discipline recorded because one instance of a class is not the class:** the population is every crate declaring an `SPT_HOME` helper INTERSECTED with the binaries that also reach `spt_test_support`; running it found a second live, unreported instance (`crates/spt`'s `testutil::LOCK` beside `cli.rs`'s direct `TestHome`). `spt-live` and `spt-store` depend on `spt-test-support` not at all, and `spt-msg` declares no helper of its own.
- **Source:** releases#111 (filed by doyle from hertz's gate observation, 2026-08-02); filed mechanism refuted and the real root measured the same day — hertz. Cousin class, same date: two builds writing one binary path with nothing guarding between them, which is why the gate recipe now identity-checks the exe hash (doyle, W2 gate).

<!-- [doc->REQ-POOL-FOREIGN-TREE-REFUSED] -->
### 5.20 Two source trees sharing one cargo target directory fabricate reds that belong to neither  `[REQ-POOL-FOREIGN-TREE-REFUSED]`
- **Failure:** a gate lane and a builder lane both built through `.worktrees/gate-target`. The gate compiled against the builder's mid-seam, half-written artifacts and reported an `E0308` in `psyche_event_turn_e2e` that **vanished on re-run** — a red belonging to neither tree, in a lane whose entire job is to be believed. The reverse is worse and silent: the builder's green was equally untrustworthy and nothing said so. Reproduced small (releases#103): two packages of the same name and version, built from two DIFFERENT paths into one pool, resolve to **one** build-script output dir and overwrite each other.
- **Why it hides:** the sharing is invisible at the point of use. A junction shows as an ordinary `target/` and nothing in cargo's output names the real directory; `CARGO_TARGET_DIR` is quieter still — it does not even leave a directory entry to notice afterwards. Both reach the same hazard, which is why the rule is about the **relationship** (this tree writing into a target owned by another) and never about a path: the incident that motivated the filing pointed `CARGO_TARGET_DIR` at the *primary checkout*, which no rule about `.worktrees/` would have covered.
- **The harm is INTERLEAVING, not simultaneity.** Cargo holds an exclusive `.cargo-lock` for a build's duration, so two builds are never literally concurrent — the second blocks. What bit was one lane's artifacts being invalidated by another **between that lane's own steps**. A lease keyed on a `cargo` process therefore guards nothing: it is stale seconds after every claim, so every crossing reads as a takeover and the refusal can never fire. That shape was designed, measured, and refused.
- **Invariant:** a pool belongs to one source tree. `crates/spt-store/build.rs` enforces it and refuses with `SPT_POOL_FOREIGN`, naming the pool, the owning tree, the lane, the holder pid and the way out. Sequential sharing is correct and cheap — a provably-gone lane is a **loud takeover**, never a refusal, because refusing it would demand a pool per lane (30+ worktrees at a measured 7-15 GB each). Staleness is proven from the **process table plus the birth stamp**; UNKNOWN is not stale and proceeds with a warning. An ownerless pool imprints its first builder, so a developer's own fresh `target/` is never refused. `SPT_POOL_UNCHECKED=1` overrides and says so on every build it allows.
<!-- [doc->REQ-POOL-LANE-IDENTITY] -->
- **A DEAD HOLDER IS NOT A FINISHED LANE `[REQ-POOL-LANE-IDENTITY]`.** The first shape of this guard answered "has that lane finished?" from the holder process alone. Measured on this box afterwards: **3 of 3 live pool claims had a dead holder, and one of those three lanes was live.** A holder is an agent session or a rig shell — cleared, re-forked, re-parented as a matter of routine — so its death says nothing about the lane, and reading it as "finished" hands the pool to a second tree **mid-lane**, i.e. reaches this very hazard *through* the guard. So the claim records the lane's **git identity** (its branch and the base sha that branch carried at claim time) and the finished question is answered by **ancestry**: in flight while the lane's tip is not contained in the integration head; settled once it is merged, once the branch is gone, or once the branch no longer carries the claimed base — a branch name re-pointed onto other work is the branch-level twin of a recycled pid. **The base sha is also the repo-identity witness:** ancestry is read in the *arriving* tree, so a branch absent from a *different* repository would otherwise read as a finished lane; an unanchored claim answers UNKNOWN, never "settled". (Found by its own test, which read `Settled: branch no longer exists` out of a directory that was not a repository at all.) **Precedence is asymmetric:** a live holder may only ever ADD a refusal, ancestry alone may declare a lane settled, and a stamp written before the identity fields existed keeps its old behaviour and says so in the line it prints. **What this arm covers, stated exactly:** the enforcer is the build script of the tree that ARRIVES, never the claim it reads, so a lane is protected when the arriving tree carries this change — coverage grows as trees turn over and is never retroactive (measured: a rig whose arriving worktree sat at the pre-identity sha took the pool over, which is the old behaviour working correctly in an old tree). A version line in the stamp cannot fix that and is refused: the tree that must refuse is the one that does not read the new field, so the reader is the problem and not the record. **Every refusal's remedy must be a command that RUNS** — `xtask` depends on `spt-store`, so any command building the tool goes through the refused pool; measured from the refusal state, a remedy naming a bare `cargo run … pool-release` failed with the identical refusal that printed it. The rule is enforced over the whole population of refusal arms, because it was learned once on one arm and re-broken by the next arm added.
- **The guard's own hazard:** the build script must emit **no** `rerun-if` directive. It is re-run because cargo's fingerprint for its unit carries `CARGO_MANIFEST_DIR`, i.e. on exactly the tree ALTERNATION the guard exists to catch. Measured on byte-identical trees, sequence `A B A A B`: no directives → 4 firings; one `rerun-if-changed` → **1**, and the foreign tree never fired it again. `crates/spt-store/tests/pool_guard_canary.rs` reads the shipped build script and reds if a directive appears — a comment would be a prohibition, not a boundary.
- **Source:** BAROMETER W1/H1 window (doyle + todlando, 2026-08-02); design measured and built by hertz, releases#103.

---

## 6. Documented regressions (non-obvious invariants)

### 6.1 No flat/nested perch siblings; resolver-routed paths
- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.
- **Invariant:** one path resolver; never create divergent siblings.
- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.

### 6.2 Soft-cleanup preserves state, removes `ready`
- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
- **Sister cite:** `src/owl/stop.rs`.

### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.

### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.
- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.
- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).

### 6.5 Direct-write precedence guard against stale LLM overwrites
- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.
- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity **plus a per-node version vector** (entries from each node's monotonic `EpochSource`; wall-clock never orders). Distributed rule (ADR-0013, M4-D6): dominate→accept, dominated→drop, **concurrent→surface as durable replicated conflict artifacts + Psyche-reconcile on the active instance's node — never silent newest-wins, never lose either version**. The freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the same guard read as vector dominance. Highest-value carryover for the sync design.
- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

### 6.6 Surfaced context conflicts preserve both versions until dominated
- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.
- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.
- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.
- **Origin:** ADR-0013 design invariant (red-team #7's "wall-clock loses concurrent writes" closed M4-D6), not a sister bug — registered ahead of the wire path so D6c is born conformant.

### 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<Broker>` + `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 disk but the running daemon keeps executing the old code until an unrelated restart/logon. The no-endpoint-drop self-update pillar (REQ-UPD-3, ADR-0004) is silently unrealized. Observed live 2026-06-09: `enlyzeam` ran 0.3.0 with 0.3.2 on disk for ~a day, still reproducing the bug the update fixed.
- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/03 productionized as `int`), NOT the in-process handoff shape that masks this regression.
- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.
- **Origin:** unintended spec/impl drift from ADR-0004 (the broker *process* was specced + spiked but built in-process), discovered during the v0.3.2 fleet update verify. Full audit + decisions: `docs/BROKER-BRAIN-SPLIT-RESTORATION.md` (verified) + ADR-0018.
<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->
- **D1 (restoration skeleton, ADR-0018 Q2/Q3):** the process boundary is restored — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` child (`brainproc.rs`); the broker survives the brain dying and respawns it (proven in production topology by `crates/spt/tests/brain_split.rs`). The logic loops still run broker-side (D2 migrates them); the `int` process-level survival E2E + the in-process re-point land at D7.
- **Closed out (2026-06-11, v0.4.0–v0.4.2):** the two-process model shipped (v0.4.0); the D7 `int` E2E (`brain_survive.rs`) + the N-1 gate prove process-level survival onto swapped bytes and re-pointed REQ-DAEMON-2 / REQ-UPD-3. The v0.4.1 fleet-verify proved this Windows-seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but exposed a **Linux** respawn-path gap — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (`[REQ-HAZARD-BRAIN-RESPAWN-PATH]`, 6.11), fixed in v0.4.2 (respawn from the canonical path captured at broker start + a promotion bytes-gate). Seamless update is now proven on **both OSes** — Windows live (hfenduleam ×2: 0.4.0→0.4.1→0.4.2, broker pid held) and Linux via the CI-gated in-place-rename E2E (`brain_respawn_rename.rs`, kitsubito runner). The fleet runs v0.4.2 on fixed brokers after the project's final two manual bounces. The process-isolation invariant holds; 6.11 is its Linux-respawn-path corollary.

### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`
- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).
- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).
- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.
- **D5 conformance (2026-06-10):** the new durable timing state `<spt_home>/deadline-<key>.json` (restoration D5-1) is **additive** — a rolled-back pre-D5 binary does not know the file and simply ignores it (re-phasing on its own flat-sleep cadence, the pre-D5 behavior). No existing-file schema migration, no irreversible pre-ready write → the new file is rollback-N-1-safe by construction. The thing for a future D6 guard to gate is a *migration* of this file's shape, not its introduction.
- **D6 guard (2026-06-10, restoration D6-3):** the invariant is now **asserted**, not just noted. The pre-ready durable writes are **enumerated in one place** — `spt-daemon::PRE_READY_DURABLE_FILES` (`rollback_compat.rs`): `deadline-<key>.json` (D5, `DeadlineAnchor`), `applied-state.json` (D6-1, the two-phase `AppliedRecord`), and the generation-stamped `brain.ready` breadcrumb (D6-1b, `{pid, generation}`). A **tripwire unit test** pins each one's additive / N-1-readable contract (load-bearing field names present; an unknown extra field still deserializes), so a *non-additive* pre-ready change (renamed/removed field, or a `deny_unknown_fields`/non-tolerant shape) trips the test and forces the migration **behind ready-promotion** (or into an additive form). **Both new D6 durable files are additive → N-1-safe by construction:** the two-phase `applied-state.json` is a *new* file a rolled-back pre-D6 binary does not know — it ignores it and falls back to the legacy `applied.json`/`last-outcome.json` (which D6 keeps writing alongside for the convergence query); `brain.ready` only gained a `generation` field its sole reader tolerates. It is a tripwire, **not** a migration framework (activate-don't-pre-fail) — the day a real migration is needed, this guard is the wire it trips.
- **Origin:** verification amendment `[V1]` (agent `doyle`) on `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.
- **Closed out (2026-06-11):** the readiness-gated auto-rollback shipped (v0.4.0) and the pre-ready durable-file registry + tripwire guard (D6-3) hold. v0.4.2 added a promotion **bytes-gate** that turns a wrong-bytes respawn into an auto-rollback rather than a false `applied` record — strengthening the rollback path the same release exercised across the fleet. No in-place schema migration has shipped, so the invariant remains **asserted, not yet exercised by a real migration** (correct — activate-don't-pre-fail); the tripwire is the wire it trips the day one is needed.
<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->

### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output
- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_through` on the live-send) and then **discarded** by the waiting loop's `_ => continue`. Cursor advanced + content dropped = that chunk is gone for the downstream consumer and the broker will **not** re-send it (resume reads from the delivered cursor, ADR-0018 D4). One session's spawn/command starves another session's output.
- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.
- **spt-core mapping:** the **other-session** face is unreachable today — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no other session to starve. The **same-session** face is reachable in direct `Brain` users: `spawn_session_pid` can consume and discard that session's own first `Output` frame while waiting for `Spawned` (measured in-process on Linux, 2026-08-25), so a caller must not treat the producer's first chunk as guaranteed post-spawn output. The other-session face surfaces the moment daemon-hosted sessions land (the live-agent adapter), which must rebuild the blocking `read_event` loop regardless (N interactive sessions cannot share one blocking reader). Recorded so that redesign inherits the constraint rather than rediscovering it. No daemon-hosted routing machinery now (would be untested dead code, activate-don't-pre-fail).
- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.

### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same constraint that moved session continuity to the broker in D4).
- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The update-vs-crash decision is the D3 spawn-time `StartReason`. **One-shot** (alarm) deadlines persist their absolute `target-time` at creation and **never reset** on any restart ("remind me at 3pm" is a commitment) — the asymmetry vs the periodic crash-reset is the rule. **[V4]** Only phase-significant loops convert; idempotent pump cadences (stagger-from-due-now) need none — converting them would re-add the per-loop writes Q4 minimizes.
- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired helper, the port builds the *scheduler*.
- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).

### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`
- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, and the daemon records `applied:N` while still running the previous version. New code does not run; the record is optimistically wrong (the enlyzeam-class record/reality divergence, now provable via `exe_hash`). **Windows** dodged it — `GetModuleFileName` returns the path string captured at process start, so the swap lands new bytes — which is why v0.4.1 went green on hfenduleam and red on kitsubito. Observed live 2026-06-11 (kitsubito ran v0.4.0 bytes under an `applied:8` record after a brain-only `apply`).
- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof the new bytes run). If either hash is absent the gate degrades to readiness-only (N-1-safe for pre-metadata releases / a missing breadcrumb) but emits `PROMOTE_BYTES_UNVERIFIED` so a disarmed gate stays field-diagnosable.
- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.
- **Origin:** ADR-0018 Q3 silently assumed `current_exe()` path-string semantics; surfaced by the v0.4.1 fleet-roll `exe_hash` bytes assert (agents `todlando` + `doyle` + `deployah`, 2026-06-11). ADR-0018 Q3 amended.
<!-- [doc->REQ-HAZARD-BRAIN-RESPAWN-PATH] -->

### 6.12 The echo-commune brief never transits the agent's drop path (one writer on `<id>-commune.md`, and it is the agent) `[REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP]`
- **Failure:** spt-core wrote its echo-commune brief to `<id>-commune.md` — the SAME path an agent writes its own boundary commune to — with no arbitration between the two writers. A Self commune authored at a `/clear` and not yet ingested was therefore overwritten **unread** by the boundary echo, which fires 25-45s later and lands exactly in that window. Measured on one endpoint 2026-09-06: **two of three Self communes lost inside one hour**, with the reborn session reporting its own wake text as stale. 6.4 named the invariant as "the LLM is read-only; only the supervisor deletes" and never contemplated core itself becoming a second WRITER on the Self's path; `echo.rs` read "the mind" as the Psyche and forgot the Self, while `docs/CONTEXT-MEMORY.md` says the Self *does* write that path at a boundary and that the Self signal is the STRONG live-context signal. So the echo — weak, project-primary, live-conservative — overwrote the strong Self capture: the exact inversion of the tiering the provenance asymmetry exists to protect.
- **Invariant:** exactly **one** writer on the Self's drop path — the agent — and the echo's brief routes **direct** into the two-tier store on the fire. The file was never required by the design; it was an implementation convenience that re-used the ingest, and re-using it cost the Self's own words.
- **spt-core mapping:** `fire_echo` (`spt-daemon/src/lifecycle.rs`) calls `spt_live::ingest_body` — the routing core shared with `ingest_drops`, so a later tiering change cannot fork — then publishes the COMMUNE io-event and consumes the write-ahead intent. `run_echo_commune` takes no directory and writes nothing on any arm. A deferred project slice (no project anchor) is a **loud discard**, never a file.
- **Testing note:** the pin is a **byte-identity** assertion over a pre-placed authored drop, not an existence check — the failure is an OVERWRITE, so the file still exists afterwards and `exists()` stays green straight through the defect. Restoring the write turns that arm red.
- **Origin:** operator ruling 2026-09-06 on doyle's own funnel measurement (releases#276); built by todlando.
<!-- [doc->REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP] -->

### 6.13 IO event sequences never reset at a byte-window boundary
<!-- [doc->REQ-HAZARD-IOLOG-SEQ-MONOTONIC] -->
- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.
- **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value.
- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.

### 6.14 Serving-registry writers must serialize across processes
<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->
- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.
- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.
- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.
- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.
- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. Linux validation passed three deterministic two-process storage regressions, 26 serving-store units, 13 servehost units, and both affected serving E2Es. Disabling only the OS lock made the selected regression fail its independent contention witness; restoring the original source passed. This is storage-process plus E2E evidence, not a captured replay of the original broker/brain schedule. Windows validation produced red-first native exit 100 at the sentinel-unlocked witness, then restored 44/44 passing tests with native exit 0; receipt: `.spt/preserved/308-registry-process-lock/windows-validation/receipt.txt`.

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.

### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).
- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.
- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.
- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).

### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.
- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.
- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.
- **Source:** codex Stage A #14.

### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`
- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type="reply|notify">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.
- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.
- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.
- **Source:** grill-with-docs 2026-06-03 (uncovered by prior design passes); sister `src/live/wrapper/claude.rs` (sandbox `["Read","Write","Edit"]` + `parse_markers`).

### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`
- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one agent's slow/hung LLM call **stall every other agent's heartbeat, commune, and reply** — the per-call timeout bounds the worst case *only* when the work is isolated; serial makes the stalls additive.
- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.
- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, "all Psyche/pulse loops" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.
- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).

### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`
<!-- [doc->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, the REQ-NET-1 identity binding) read from the **broker's conn/stream table** (`NetStreamInfo::remote_id_hex`), never from payload. Wire records carry **no origin field by design**; a forged one decodes as an ignored unknown field and influences nothing. `from` inside a record is reply-routing metadata only — never an authorization subject.
- **spt-core mapping:** `spt_net::net::wanmsg::WanMessage` (no origin field) + `spt_daemon::wan::receive_wan` (origin parameter sourced from the stream table) + every future wire-inbound consumer (D5b attach, D5c transfer, D8 notifs).
- **Source:** M4-D5a design (ADR-0009 consequence: the gate must bind to the transport identity).
- **Mesh note (ADR-0017, 2026-06-08):** preserved verbatim. The mesh never relays third-party rows, so a record's author is **always** the QUIC-handshake-proven connection origin. The new **membership proof** (seed-proof) is itself channel-bound to both handshake-proven pubkeys — the authorization subject stays transport truth, never payload.
- **Sender-stamp note (W2b, `REQ-MSG-SENDER-STAMP`, 2026-07-29):** the invariant above is preserved for the **origin node** and for **`from`**, and both sentences stand verbatim — the gate's node subject is still transport truth, and `from` is still never an authorization subject. One field is now carved out of "a forged one decodes as an ignored unknown field and influences nothing": `WanMessage.sender_proven` IS decoded and **acted on** as the access chain's tier-1 sender subject. It does not weaken this hazard, because it is **daemon-stamped from the sending session** (never `--from`, which a caller controls) and it can only ever name a *sender endpoint* — never the node. Forging it therefore requires a malicious member node, which `REQ-MSG-6`'s ratified boundary places out of scope, while the in-scope adversary (an **agent** passing `--from`) is defeated. Absence abstains, so the tier's activation changed no N-1 decision. See `docs/FAULT-MATRIX.md` row 11b.

### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`
<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->
- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** where the broker stayed responsive to `daemon status` while the pump thread sat dead.
- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_timeouts()`) and its `set_nonblocking` uses deprecated `PIPE_NOWAIT`, which corrupts mid-stream (proven by the mesh E2E). Blocking reads work on both OSes; the channel supplies the deadline. A read that exceeds the deadline returns `io::ErrorKind::TimedOut`, treated as a **POISONED client** (a late reply could bind the wrong stream id in a later call — gotcha-#1-adjacent): it BUBBLES out of the round → `run_peer_pump` returns Err → `supervise_pump` restarts the pump with a fresh brain client + conn cache + reset WorkerLasts (the §V4 stagger re-primes every leg). NEVER a per-peer retry. An ordinary error (the broker REPLIED with one) stays a per-peer abort + conn drop + redial. **Leak watch (accepted, not fixed):** a restart abandons the old reader thread parked in `read_frame` (split halves share one OS handle → on a named pipe the broker may never signal the disconnect); bounded to ONE thread per actual wedge (the post-restart conn cache is fresh — the dead peer is re-dialed, not re-wedged), diagnosable by the reader's spawn/exit log lines, and fully cured by the broker-side B-half (§7.8, `REQ-HAZARD-BROKER-QUIC-DEADLINE`), shipped in v0.8.3 — with the broker bounding its QUIC await, the brain's read returns promptly (an ordinary error) so the reader thread is never left parked.
- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).
- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.

<!-- [doc->REQ-BRAIN-READ-BOUNDED-PER-CALL] -->
**Per-call enforcement (releases#293).** Every Brain reply-wait loop computes
`call_deadline()` once, after sending its request and before entering the loop,
and passes that same value to `read_event_until`. Unrelated frames do not renew
the reply budget. The unbounded `read_event()` convenience method is removed:
callers must name their deadline explicitly. Whole-carrier callers retain
`None` and their existing blocking semantics; this change does not invent a
timeout the carrier cannot honor.
Stream loops (`serve_*_feed` and the attach serve loop) are not reply-waits:
they use an explicit `None` for their subscriber-owned lifetime, independent
of the carrier's call budget. Any future silence bound is per read, not per call.
`xtask brain-read-check` enforces the retired-reader and stream-budget policy
against crate sources, including tests, and checks representative reply-wait
positive controls. The CI lint job runs this command on code-changing PRs;
`xtask check` also includes it. Predicate tests use synthetic Rust text, while
runtime recovery remains covered by separate pump-deadline integration cells.
The six WAN stream reply-waits in `wan.rs` follow `REQ-WAN-REPLY-BOUND` (a budget
renewed only by data on the matching stream) and are EXCLUDED from the audit's
fixed-deadline policy controls by record: a tokenizer cannot prove that a guarded
re-arm is the only renewal, and a false assurance is worse than a named exclusion.
The exclusion does not imply the renewal behaviour is tested (register follow-up).

### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`
<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->
- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.
- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded live queue), so a viewer attaching to a busy session is not spuriously evicted.
- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).
- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).

### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`
<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->
- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).
- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-peer arm (drop conn + redial next tick), the round CONTINUES and the heartbeat keeps advancing. **Never the brain's read-deadline:** that `TimedOut` is the 7.6 poison → supervised-restart path this fix exists precisely to AVOID. **Exactly-once preserved:** a timed-out journaled op fails INSIDE its `apply_once` closure (the QUIC call is made there), so no phantom `conn_id`/`stream_id` is recorded (`effect()?` propagates before `applied.insert`) and a fresh tick re-dials cleanly — no dedupe into a dead conn. **Happy path unchanged:** a live peer completes with zero added latency; the bound only bites a non-responsive peer.
- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run_peer_pump` exits Ok, no supervised restart). The `peer_outcome` ordinary-vs-`TimedOut` tier-split itself is unit-covered at 7.6.
- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.

### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`
<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->
- **Failure:** the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004's no-terminate-during-update pillar forbids auto-killing it — 6.7). A self-update that changes a daemon-state WIRE FORMAT — e.g. the v0.9.0 adapter-agnostic `Seed` (the `adapter` field dropped) — therefore lands a NEW-version CLI talking to the STILL-RESIDENT OLD broker. The old broker cannot deserialize the new `Seed` (its formerly-required `adapter` is absent), so it drops the seed-control conn without acking; the CLI's `put_seed` ack-read hits EOF and surfaces a raw `UnexpectedEof` "failed to fill whole buffer" — a cryptic footgun that hides the real cause (perri PREP-4 FINDING 1: v0.9.0 CLI ↔ stale 0.8.x broker).
- **Invariant:** (a) spt-core surfaces an ACTIONABLE diagnostic on the seed-ack EOF — naming the stale-broker cause + the fix (`spt daemon stop`; the broker restarts on the next `spt api` call) — never the bare io error (scoped to `UnexpectedEof` on the seed-ack path, so it never mis-fires on an unrelated error like a refused connect). (b) A daemon-state wire change requires a DELIBERATE full broker restart; this is NOT automatic — ADR-0004 forbids auto-killing the resident broker, and a brain-only update keeps the old broker. (c) FORWARD discipline: daemon-state / `Seed` schema changes stay ADDITIVE + serde-default, so a resident OLD broker tolerates a NEW CLI across a brain-only update. (This would NOT have rescued 0.9.0 itself — the old broker's `adapter` was a REQUIRED field, so no additive tolerance could read the new bytes; for a wire change that removes/renames a required field, the operative rule is the broker restart.)
- **spt-core mapping:** `startup::seed_fail_message` (the `UnexpectedEof` → actionable-hint branch) fired from `cmd_seed`'s `put_seed` error arm; the broker's seed-control residency (`seedmap::serve_seed_control`, held across brain restarts — 6.7). Unit: `seed_fail_eof_gives_actionable_stale_broker_hint` (EOF → the `spt daemon stop` hint naming the stale broker; a non-EOF kind → the plain message, no mis-fire).
- **Source:** perri PREP-4 FINDING 1 (v0.9.0 dogfood) → v0.9.1.

### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`
<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->
- **Failure (Windows):** `spt endpoint run` autostarts the daemon (`ensure_running` → `detached_no_inherit`), which INHERITS the launching terminal's Windows Job Object. Windows Terminal / VS Code place the shell AND every descendant in a Job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; closing the tab drops the job's last handle → the OS reaps the daemon + its broker-spawned ConPTY harness subtree. The `rc` pump detaching should end ONLY the viewport; the harness must keep running + stay re-attachable. (ConPTY isolation itself is already correct — portable-pty builds the pseudoconsole in the daemon; the leaking lifetime binding is the Job Object, not the console.)
- **Invariant:** both daemon spawn paths request `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000), best-effort with an in-job FALLBACK on `ERROR_ACCESS_DENIED`(5) / `ERROR_INVALID_PARAMETER`(87) so a breakaway-denying job NEVER regresses the spawn. **A real job CAN deny breakaway** (proven: the cargo/CI runner's own ancestor job ACCESS_DENIES it), so efficacy against the real terminal is UNKNOWN until measured. **int stage = OPERATOR MANUAL ACCEPTANCE, not CI** (doyle ruling 2026-06-18): the runner sits inside a breakaway-denying job and every test-created job nests inside it → a faithful "harness survives tab-close" test is a guaranteed FALSE-RED; the two units (escape-where-permitted self-skip + fallback-where-denied) are the CI evidence; `required_stages` stays `[doc,impl,unit]`. The daemon-OWNED harness Job is the **L4 daemon-stop reap** backstop ONLY — NOT tab-close survival (a job nested in the terminal's kill-on-close job dies with it). **Backstop candidate (design-only, build ONLY if the operator measures breakaway DENIED + daemon dies):** re-parent the cold-start daemon spawn OUT of the terminal job via a job-neutral creator — WMI `Win32_Process.Create` (owned by WmiPrvSE, outside the terminal job; synchronous, returns pid) preferred over a `schtasks` one-shot.
- **spt-core mapping:** `daemon.rs::detached_no_inherit` + `deelevate.rs::create_with_token` (the breakaway flag + fallback). Units: `detached_no_inherit_falls_back_under_a_breakaway_denying_job` (no-regression — the shared runner exercises the fallback directly), `breakaway_spawn_escapes_a_kill_on_close_job` (the OS escape mechanism — self-SKIPS where the ancestor job forbids breakaway). Unix: the daemon's `setsid` session-detach already keeps a closing terminal's SIGHUP off its children (guard test, no code).
- **Source:** v0.12.0 real-harness defect (operator) → v0.12.1 L1 @5ae68f8; doyle design+int ruling 2026-06-18.

### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`
- **Failure (hypothesized, v0.12.0):** a legitimately dead PTY child (crash/kill) plus an `rc` pump dropped without a clean detach (closed tab) was thought to make the broker's loopback forward `write_all` block forever on a full 64 KB duplex, park a worker in the 2-worker net runtime, saturate both, and stall every new attach / `endpoint run` (with `daemon stop` unable to join).
- **Invariant — DISPOSITION = PROVE-DON'T-CHANGE** (doyle GATE-PASS @e883f45, 2026-06-18): the post-L0 code ALREADY prevents the wedge; NO fail-fast / worker-count code was added. (1) `serve_attach` forwards fire-and-forget (`net_stream_send` `op_id=None`) and the broker-side `send_stream` is already deadline-bounded (`bounded_block_on`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s — hazard 7.8), not forever. (2) the loopback duplex is drained broker-INTERNALLY by the operator row's OWN read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream (`retentive_cap == 0`) NEVER parks → `peer_w` never backs up on a dead `rc` (a dead `rc` is just a dropped IPC subscriber against a bounded, EVICTING ring). (3) `bounded_block_on` = `runtime.handle().block_on` → parks the BROKER DISPATCH thread, not a net worker, so worker-pool exhaustion cannot occur. A dead spt-hosted endpoint is OFFLINED within one reconcile tick on abrupt child death (the broker exit-waiter `wait()` reaps the session even on `taskkill /F` → `reconcile_hosted_liveness` clears the `status=online` latch).
- **spt-core mapping:** int `crates/spt/tests/attach_wedge_e2e.rs` (REAL detached daemon + dummy-harness fixture): serve the victim (rc sees its tick), abruptly kill rc (dropped pump) + kill the PTY child → a NEW endpoint still comes online + is served (no wedge), the dead endpoint is offlined within a tick (`LIVENESS_RECONCILE_OFFLINE`), `daemon stop` bounded. Leans on 7.8 (the QUIC-op deadline) + 7.7 (viewer isolation).
- **Source:** v0.12.0 real-harness finding (operator) → v0.12.1 L2 @e883f45; doyle GATE-PASS 2026-06-18.

### 7.12 Controller output must NOT be written inline on the drain thread — a backed-up controller wedges the session  `[REQ-HAZARD-INJECT-CONTROL-COEXIST]`
- **Failure (v0.12.x, operator dogfooding + doyle /diagnose 2026-06-19):** `OutputLog::append` fanned each live chunk to the CONTROLLER via a SYNCHRONOUS, blocking `write_frame` held INLINE on the session's single drain thread while `Mutex<OutputLog>` was locked (viewers already had a dedicated writer thread + bounded evicting channel). A backed-up controller socket — a slow operator, or the full 64 KB `rc` loopback duplex under heavy TUI redraw — parked the drain thread WITH THE LOG LOCK HELD: output + keystroke-echo stalled and every attach / resize / `KIND_SESSIONS` that needs the log lock blocked FOREVER, while the broker process stayed alive. Fires on NORMAL interactive `rc` use under heavy output (not only message injection); REOPENED the wedge facet of 7.11 (dead-child backpressure only).
- **Invariant:** controller delivery runs OFF the drain thread — a dedicated controller writer thread + bounded channel (the 7.7 viewer_writer pattern) does the blocking socket write; the drain hands each chunk off with a BOUNDED, OFF-LOCK send (deadline → detach + `clear_controller`, never park forever, never silently evict a LIVE operator's authoritative view). The controller is AUTHORITATIVE (unlike a viewer): its writer advances the `delivered_through` resume cursor (atomic, only-on-success, monotonic). `clear_controller` is the ONE unlatch path (clears `driven_by`, drops the sink) — W5's reconcile self-heal reuses it.
- **spt-core mapping:** impl `broker.rs` `OutputLog::append`/`controller_writer`/`become_controller`/`ControllerJob::deliver`/`clear_controller`; int `crates/spt-daemon/tests/inject_control_wedge.rs` (a fully-backed-up controller keeps `KIND_SESSIONS` answering = no wedge); unit the bounded-deliver + monotonic-cursor kernels. The input-side parks (write_input `write_all` on a full buffer; DSR-answer writer-mutex contention) are BENIGN on Windows ConPTY (absorbs a large inject), real only on Unix forkpty → bounded by W2's atomic-write substrate.
- **Source:** v0.12.x operator wedge → v0.13.0 W1 (doyle GATE-PASS 2026-06-19).

### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`
- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.
- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)
- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practical effect is the Backspace/Ctrl+Backspace native mapping.
- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.
- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).

### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`
- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefinitely, so the single-threaded dispatch could open NO attaches → every `spt rc --view/--take` died with `brain IPC read deadline elapsed` (broker control-plane KIND queries still answered = a different thread). DISTINCT from 7.12 (the OUTPUT drain); this is the INPUT/effect-journal path 7.12 never touched. **Refutes** 7.12's "input parks are Windows-benign (ConPTY absorbs)" deferral — on the real box the input path wedges regardless, because the wedge is the LOCK HOLD, not the PTY write blocking per se.
- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/registry/net) keep their fsync'd markers. The PtyWrite itself must additionally be bounded/fail-fast and must not hold the writer mutex across a blocking write (the Unix-forkpty park bound; ConPTY absorbs = benign).
- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of the no-fsync path, proven by the structural unit.
- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).

### 7.15 An OFFLINE spt-hosted endpoint must NOT render phantom `ONLINE+CONTROLLED` — clear `driven_by` when its session is gone  `[REQ-HAZARD-DRIVEN-BY-SELFHEAL]`
- **Failure:** `driven_by` (the `info.json` `ONLINE+CONTROLLED` latch) is single-written by the broker via `clear_controller`/`stamp_driven_by`, which only fire on a controller change. When an spt-hosted endpoint's broker session is GONE (harness dead — the B2 case, 7-series sibling `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE`), no controller event ever fires, so a stale `driven_by=Some(node)` persists: the picker renders a phantom "controlled by X" on an endpoint that is actually OFFLINE. The B2 reconcile already clears the `status=online` latch here but left `driven_by` untouched.
- **Invariant:** `reconcile_hosted_liveness`, when it offlines a sessionless controllable perch (the B2 keystone — no live broker session ⇒ dead harness), ALSO clears `driven_by` (`set_driven_by(perch, None)`). RACE-FREE and single-writer-safe: with NO live broker session there is no controller to re-stamp `driven_by` concurrently, so the brain may write it here without contending the broker. (The LIVE-session leg — a controller gone while its session survives — is NOT this hazard: a clean disconnect already self-heals via `detach_if`→`clear_controller`, W1 (7.12) bounds the active-output wedge, and the residual idle-wedged-REMOTE case is the deferred `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`, which needs BROKER-SIDE liveness eviction — a brain reconcile can NOT detect it, since the broker still reports `controller_by==Some` on an idle wedged controller. Repro-proven: `inject_control_wedge.rs` w5_a2.)
- **MEASURED 2026-07-22 (DAEMON-LIFECYCLE W2 Leg B, `transport_death_eof.rs`) — the residual SPLITS in two, and only one half is still open.** On REAL QUIC (two brokers, `BindScope::Loopback` + a real `net_dial` — NOT the in-process duplex, which has no idle timeout at all), a remote controller whose QUIC STACK DIES WITHOUT A FIN (peer frozen: endpoint alive, UDP port still bound, keepalives unanswered — the sleeping-laptop shape) DOES self-heal: the transport's own idle timeout tears the conn, the read pump's `Err(_) => finish()` leaf surfaces `NetStreamEof` to the ALREADY-SERVING `serve_attach` worker, and the seat releases. **The numbers: clean FIN 120ms · torn no-FIN 65057ms** (`MESH_MAX_IDLE` = 60s plus quinn's PTO slack — a number materially UNDER 60s would mean the staging leaked a signal). **The latch is NOT a lockout — read that sentence before this one:** a different-conn attach during the window is an ADR-0038 fix-6 successor and takes the seat IMMEDIATELY, so the returning operator (or a second one) is never blocked by the dead seat. The residual harm is stale STATUS TRUTH (`CONTROLLED`/`driven_by`/viewer count) for ~65s, self-healing on a bound. **DECIDED 2026-07-22 (doyle, ON the number): ACCEPT AS DOCUMENTED** — no presence-FIN synthesis (a second teardown mechanism on this seam to shave ~60s off a case that already converges) and no keepalive tune (a global QUIC tradeoff: chatter, battery, mobile paths). The revisit lever is NAMED, not built: if the window ever reads as bad product feel, the keepalive interval is the knob, and it is a product-feel call with a false-evict bound to design. Grounds in full: ADR-0040 Amendment 1. **Scope of the measurement:** local real QUIC (two brokers, one box); LAN confirmation over a real two-host path is outstanding and rides the next box-occupancy window — insurance on a decision that builds nothing, not a dependency. **The still-open half is the ALIVE-but-WEDGED controller (A2): its QUIC stack keeps answering keepalives while only the app writer is parked, so the transport NEVER dies and no idle timeout ever fires** — nothing in the transport-death class reaches it, and the measurement above must not be read as covering it.
- **Watch-out (repro-proven, real broker):** `SessionInfo.controller_by==None` is AMBIGUOUS — `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None`, so a live LOCALLY-driven session also reads `None`. It is therefore NOT a usable standalone `driven_by` clear trigger (would false-clear a live local session). The shipped Gap-B self-heal needs no controller signal at all (it keys on session ABSENCE).
- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).
- **Cross-ref 7.29 (who MAY clear vs who MUST NOT):** the BRAIN reconcile here MUST NOT clear `driven_by` off `controller_by==None` (ambiguous with a live LOCAL controller). The BROKER stamp-convergence (7.29) MAY — it reads `has_controller()` DEFINITIVELY (its own controller slot), so it authoritatively clears a stale `driven_by` against a live session with no controller (the `has_controller()==false` leg). The invariant's owner for that leg moved to the broker; this brain-side ambiguity-safety is unchanged. The wedged-open-remote A2 residual (`has_controller()==true`) is NOT covered by either — still `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.
- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.

<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->
### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`
- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).
- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to the byte path (keeps the e2e byte-injection working). UNIX UNCHANGED (cfg-split; its raw-mode stream already delivers VT). SUPERSEDES 7.13's `normalize_key_byte` swap on Windows — Backspace → `0x7f` and Ctrl+Backspace → `0x08` are emitted NATIVELY by the translator.
- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).
- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).

<!-- [doc->REQ-RC-WIN-PASTE] -->
### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`
- **Failure (operator dogfooding):** in an `spt rc` session neither ctrl+V nor right-click pasted (CC explicitly supports ctrl+V). `RawGuard` did only `enable_raw_mode` (no bracketed paste, no mouse capture, no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm `Event::Paste`), and ctrl+V translated to a bare `^V` forwarded to CC — but **CC runs daemon-side with NO access to the operator's LOCAL clipboard**, so remote paste is fundamentally CLIENT-ORIGINATED. A multi-line paste-as-keys also became a `\r` submit-storm.
- **Invariant:** on Windows (cfg-split; folds into the 7.16 event path), on a RIGHT-CLICK rc reads the LOCAL clipboard itself and forwards a BRACKETED paste — `wrap_bracketed_paste` = `ESC[200~` + content + `ESC[201~`. CC has bracketed-paste mode on (its TUI sets `ESC[?2004h`), so it treats the synthesized markers as a PASTE: content lands intact, NO submit-storm, harness-AGNOSTIC (standard xterm contract). `RawGuard` also `EnableMouseCapture` (disables console QuickEdit + enables `ENABLE_MOUSE_INPUT` so a right-click surfaces as `Event::Mouse`) on an interactive console only, restored on drop → right-button-down → `read_clipboard` → bracketed paste. `read_clipboard` = the `clipboard-win` crate; empty/failed read ⇒ a clean no-op (never inject garbage, never panic). Content forwarded VERBATIM (literal pasted text, no per-char translation). UNIX UNCHANGED (its terminal pastes natively through the byte pump). **ctrl+V is NOT intercepted** (P1b amendment, HITL re-open): Windows Terminal CONSUMES ctrl+V as its own paste accelerator — it delivers only a Key `kind=RELEASE` (never a Press, so a Press-guarded arm could never fire) AND injects the clipboard as a char-by-char KEY FLOOD spt-core cannot intercept. ctrl+V now rides WT's native paste as keystrokes (multi-line may submit-storm — acceptable; bracketed fidelity = right-click). The flood landing as keystrokes no longer wedges the broker (7.19).
- **spt-core mapping:** impl `rc.rs` `wrap_bracketed_paste` + `mouse_is_paste` + `clipboard_paste` + `read_clipboard` + `windows_mouse_wanted` + `RawGuard` (mouse capture/restore) + the `spawn_stdin_reader_events` right-mouse arm; unit the exact bracketed framing + content-verbatim + mouse classify (right-down ⇒ paste, all else ⇒ drop) + the injected-reader paste decision (non-empty ⇒ wrapped, empty/fail ⇒ no-op); NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent).
- **Source:** v0.13.0 P1 (operator HITL; doyle design 2026-06-19). Depends on P0 (7.17). AMENDED v0.13.0 P1b: scope narrowed to right-click-only (the dead ctrl+V interception removed); scroll-forward is 7.20; the input-flood non-wedge is 7.19.

<!-- [doc->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] -->
### 7.19 An operator input FLOOD must not deadlock the broker via the applied-ack on the same conn  `[REQ-HAZARD-INPUT-ACK-BACKPRESSURE]`
- **Failure (operator HITL, the ctrl+V re-open):** a flood of operator input on one brain↔broker conn wedged the WHOLE broker PERMANENTLY (no new/existing attach; the controller stayed latched — the per-conn handler couldn't process the detach). `serve_attach` processes a whole `NetStreamData` batch of N `Input` records in its inner loop, calling `send_effect` N times WITHOUT returning to `read_event()`; the broker answers each with `send_frame(applied_envelope)` on the SAME conn. Brain not reading → the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block → mutual full-duplex DEADLOCK. (Capture: 11 frames, `write_input` 11/11 — P0 holds; `ack send` START=11/END=10 — frame #11's ack never returns.) WT's ctrl+V paste-accelerator key-flood was the trigger; the deadlock is generic to ANY input flood.
- **Invariant:** the applied-ack is OPT-IN. `InputReq` carries `ack: bool` (serde `default = true`, N-1-safe). The fire-and-forward operator/rc path (`serve_attach`) sends `ack=false` via `Brain::send_effect_no_ack`; `dispatch_input` writes NO applied frame when `ack=false`, so the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` (one-at-a-time spool delivery, WAITS on `BrokerEvent::Applied`) keeps `send_effect` (`ack=true`). EXACTLY-ONCE preserved: the broker dedups by `(session, op_id)` at the applied-set regardless of the ack. **N-1 caveat:** an OLD resident broker (the self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inherent broker-resident-wire-change class, see 7.9).
- **spt-core mapping:** impl `msg.rs` `InputReq.ack` (`default_true`) + `brain.rs` `send_effect_no_ack`/`send_effect_inner` + `attach.rs` `serve_attach` operator path → `send_effect_no_ack` + `broker.rs` `dispatch_input` gates `send_frame(applied)` on `req.ack`; unit the serde default + the ack-emitted-iff-true + the no-ack-path exactly-once dedup; int (keystone, repro-first) a flood of N>pipe-buffer input frames through `serve_attach` on one conn — PRE-FIX deadlocks, POST-FIX drains all N + the session stays live + a concurrent attach opens (real broker+brain, no mocks).
- **Source:** v0.13.0 P1b (operator HITL re-open; doyle /diagnose on the enhanced rc+broker capture 2026-06-19). The real ship-blocker behind the ctrl+V wedge.
- **Refresh reconstruction (releases#49/#267, 2026-09-16):** the live-input branch already used `send_effect_no_ack`, but the pre-`Subscribed` buffer still drained through acked `send_effect`. The real Windows refresh rig retained 28 baseline inputs plus three sent during brain replacement. Atomic probes measured the replacement worker blocked writing pending input 4 while the broker handler blocked returning `Applied` for pending input 1; both returned only when the physical connection was poisoned about 15 seconds later. Both `Subscribed` replies had already returned; outbound forwarding was not the blocked call. `send_effect` itself does not wait for an ACK: the reciprocal writes form the cycle.
- **Remedy and ownership:** both live input and pending replay use the existing no-ACK path. The broker still applies the original operator-minted keys exactly once, and no input is injected before the controller grant. Viewer/busy handling, controller-generation fences, timeouts and the `by:Some` promotion exclusion are unchanged. No new thread, queue or lifecycle owner is needed.
- **Why containment was insufficient:** connection poison/cancel/sever, cursor-only brain resume, off-lock stream replay and heartbeat-driven reap still do their jobs; they cannot make this write-only batch drain its return carrier. Retrying reconstructs the same retained-input batch and repeats the cycle. A promoted brain is not proof that an operator-owned controller has recovered. Older tests covered live-input flooding, two-input manual restart, or a deliberately rolled stream ring; none combined the real refresh path with retained input history. The Windows `refresh_freeze` regression retains that workload and requires bounded child ACK plus independent state, zero replacement write-poison, and detach-only closures.

<!-- [doc->REQ-RC-MOUSE-FORWARD] -->
### 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 `ESC[?1000h/1002h/1003h` (mouse on) + `ESC[?1006h` (SGR ext) and their `…l` (off) into a shared `MouseMode{enabled,sgr}` (the pump writes from the output render path, the 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 (7.18, unchanged); `ScrollUp/Down` → an xterm SGR mouse report (`ESC[<64;col+1;row+1M` up / `ESC[<65;…M` down; 0-based crossterm → 1-based xterm), forwarded ONLY when `enabled && sgr` (else DROP — a legacy report the harness may misread is garbage); Moved/drag/left/middle DROP (scroll is the need; click-forward risks garbage, no click-to-position). UNIX UNCHANGED (no capture; the terminal scrolls natively).
- **spt-core mapping:** impl `rc.rs` `MouseMode` + `MouseModeScanner`/`parse_decset_private`/`apply_mouse_mode` (carry-buffer scan, fed from the pump's output path) + `scroll_dir` + `scroll_sgr` + the `spawn_stdin_reader_events` scroll arm (threaded via `Arc<MouseMode>`); unit `scroll_dir` classify + `scroll_sgr` exact bytes + the DECSET scan (set/reset, combined, mixed, and split-across-chunks); NO int (live console mouse = HITL).
- **Source:** v0.13.0 P1b (operator HITL; doyle design 2026-06-19). Bundled with 7.19.

<!-- [doc->REQ-HAZARD-CONTROLLER-WRITER-REORDER] -->
### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`
- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the strict consumer saw `output gap: got seq 1 want 0` → `attach_survives_target_brain_restart_exactly_once` panicked at `.expect("re-serve")` OR HUNG in `render_until` (serve thread died on the gap → `MARKER_TWO` never reached the wire). `prior.next_seq` is life1's CONSUMPTION cursor (life1 forwards each frame to the operator immediately on consume, so at crash it has forwarded exactly `[0, K)`); life2 resumes from that same `K`, so the boundary aligns and `[0, K)` need never be re-sent. The crash was NOT byte loss — it was the consumer running the strict **reject-gap legacy path** (handoff left `session_cursors` empty) which treats any out-of-order seq as fatal. PRE-EXISTING, surfaced by the v0.13.0 green-both-runners gate; P1b is innocent. Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`.
- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix #1 — "drop handoff's eager subscribe" — was REVERTED: that subscribe is the standalone-resume mechanism the brain-only update engine + `handoff`/`idempotent`/`daemon_e2e` replay through with no `serve_attach`): (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs dedup-below + snap-above (resume mode), never the reject-gap legacy trap; the ascending-merge property above makes this complete, not merely tolerant; (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is EPOCH-GATED: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded, so a superseded writer can never flush its stale replay past the bump (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is deliberately NOT gated — new output only ever flows to the CURRENT controller's channel, so a superseded writer's channel holds only its pre-supersede backlog (deduped by snap-above) plus its TERMINAL `Displaced` kick, which the displaced controller MUST still receive; that loop ends naturally on `tx`-drop (gating it suppressed the loud-take `Displaced` — the cv-matrix hang); (3) EXPLICIT-RESUME / OPERATOR-STREAM BOUNDARY (the load-bearing fix — kitsubito RACEDIAG ~33% repro that the keystones missed) — `Brain::subscribe_with` (shared by `attach` AND `attach_as`) RESETS that session's dedup cursor to `from_seq` in resume mode. WHY it's load-bearing, not just the ground-truth re-read: the handoff's eager `subscribe(K)` makes `serve_attach`'s `brain` receive the replay frame at seq=K BEFORE the operator's `Request` is processed (`attached` still false); that early frame is dropped by the `if attached` forward gate but the snap-above cursor has already advanced past K, and `attach_as(sid,0)`'s re-subscribe used to leave the cursor advanced — so the broker's re-send of seq K arrived below it and was deduped → seq K never reached the operator viewport → a `no forward gap` panic at the operator render cursor (`render_until`), and SILENT content loss in the real `rc` consumer (dedup-below + snap-above). Resetting to `from_seq` on the `attach_as(0)` re-subscribe makes the broker's full re-send re-deliver from 0 (the operator dedups the overlap), so seq K is forwarded. The epoch gate (2) is sound (RACEDIAG: zero socket interleaving above K); the residual was purely this consumer-side boundary. Cold-start brains (empty map — e.g. the production dispatch serve brain) keep the legacy `next_seq` path untouched, so production is unaffected.
- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::subscribe_with` (resume-mode dedup-cursor reset to `from_seq`, shared by `attach`/`attach_as` — the operator-stream boundary fix) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.rs` + `tests/attach.rs`) deterministically force two `become_controller`-on-one-connection on a real broker+brain (no mocks) — PRE-FIX reorders/gaps, POST-FIX monotonic + byte-exact + session live, PLUS `attach_survives_target_brain_restart_exactly_once` green (doyle: 20× isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier).
- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected across two gate rounds — fix #1 reverted, then the kitsubito RACEDIAG pinned the residual to the consumer-side operator-stream boundary, fixed by the `subscribe_with` cursor reset on `attach_as` re-subscribe). The last v0.13.0 ship-blocker.

### 7.22 An idle delivery with no working translation binary must SPOOL, never raw-inject a pseudo-delivery reported as delivered  `[REQ-HAZARD-IDLE-SILENT-NONDELIVERY]`
- **Failure (F-019 post-mortem):** the v0.11.0 path raw-injected `payload+\r` into the spt-hosted PTY whenever no working translation binary handled an inbound message — none declared, spawn-failed, FAULTED (commit-deadline miss / binary death), or its inject-worker channel dropped — AND acked `delivered=true`. But a bare `payload+\r` does NOT submit on a modern TUI (Claude Code): the message was TYPED into the harness but never sent, a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is precisely what MASKED F-019 (`[REQ-INSTALL-11]`, above) through a multi-hour black-box hunt — every behavioral hypothesis was moot because the "delivery" never delivered.
- **Invariant:** spt-hosted idle delivery is translation-binary-ONLY (ADR-0022 amendment). `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` and writes NOTHING to the PTY; the caller (`try_broker_inject` → `cmd_send`) reads `delivered=false` and falls through to `deliver::send` = SPOOL (poll-fed, never lost), reporting an honest `QUEUED`, never a confident-but-false `SENT`. The failure is LOUD (`ENDPOINT_INJECT:<ep>: no working translation binary … -> SPOOLED, not injected`). The raw-inject fallback (`input.enqueue`) is removed from the no-binary, worker-dropped, AND post-fault paths; the operator-keystroke floor-flush on FAULT is UNCHANGED (operator input is never stranded). Out of scope (follow-up): broker-side auto-redrive of already-spooled inbound when a live-update binary spawns (ordering/exactly-once hazards; the poll substrate + subsequent sends cover re-delivery).
- **Boundary — what this does NOT cover (the STATE-vs-transient precision):** the guarantee is the steady FAILED STATE — once a binary IS faulted/absent/worker-gone, subsequent deliveries spool (`faulted` is MONOTONIC: set once, never respawns, so the state is reached deterministically). It does NOT cover the FAULT-TRANSIENT: a delivery that lands in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. This is a SEPARATE, PRE-EXISTING hazard (raw-inject removal did not touch it — the old code dropped that queued event too; v0.14.3 makes nothing worse) and is tracked for **v0.15.0 under `[REQ-MSG-DELIVERY-AXES]`** (the spool-centric delivery redesign: ack-on-SPOOL replaces ack-on-enqueue, which closes the optimistic-ack drop naturally). The g2 int gate asserts the steady state via **bounded-retry-until-spool** (faulted-monotonic → converges) rather than a single-shot ack, so it is load-robust under the parallel CI suite.
- **spt-core mapping:** impl `broker.rs` `dispatch_endpoint_input` (no-working-binary → `delivered=false` + loud log, no `input.enqueue`; the `input` writer is no longer resolved from the sessions table) + `build_translation`/`fault_translation` (`None`/fault now MEAN spool); unit `msg.rs` `endpoint_injected_envelope_carries_delivered_both_ways` (the `delivered=false` spool signal round-trips); int (real broker+PTY) `tests/broker.rs` `endpoint_keyed_inject_without_binary_spools_not_pty` + `tests/inject_control_wedge.rs` `large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging` + `g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input` (post-FAULT inbound EVENTUALLY spools via bounded-retry-until-spool, marker NEVER on PTY across attempts — load-robust; the in-window fault-transient is the carve-out above).
- **Source:** v0.14.3 (ADR-0022 amendment @1eaeef1, operator-ruled; doyle-scoped, the F-019 follow-up). CHANGE-5 (`cli.rs` honest report) verified a NO-OP — the spool fall-through already reported `QUEUED`, never `SENT`.
<!-- [doc->REQ-HAZARD-IDLE-SILENT-NONDELIVERY] -->

---

### 7.23 A message that has REACHED a node's spool must NEVER depend on an adapter hook-poll cadence to reach an spt-hosted (relay-less) endpoint — the daemon drives delivery on the events it owns  `[REQ-HAZARD-DELIVERY-STARVATION]`
- **Failure (F-023, RCA @ `f023-f024-wan-idle-starvation`):** a message that has already landed in a node's spool — WAN-arrived, or locally spooled-while-active — stranded INDEFINITELY on an spt-hosted, relay-LESS endpoint whenever the adapter's hooks went quiet (an idle session with no user turns). An spt-hosted perch has no `api listen` relay to wake it, and NOTHING in the daemon re-offered a spooled message once the arrival moment passed — delivery silently depended on the adapter happening to call `api poll`. Presented as "sent but never lands" with a HEALTHY translation binary and a visibly-idle perch (F-023: verified 7 min idle, binary alive, zero injection). TWO gaps: (1) the WAN-ingress path (`receive_wan`) tried the harness-hosted TCP relay then fell straight to spool — it had NO spt-hosted broker-inject leg (that leg existed ONLY in local `cmd_send`); (2) NO idle-edge drain existed anywhere, so a spooled-while-active (deferred) row was never re-offered when the endpoint next went idle.
- **Invariant:** the daemon DRIVES delivery of an already-spooled message on the events IT owns — never on hook cadence — for a relay-less spt-hosted endpoint. TWO daemon triggers feed ONE shared spt-hosted inject leg (`spt_daemon::inject::try_spt_hosted_inject`, translation-binary-ONLY): (1) **WAN ingress** — `receive_wan` injects via the live binary BEFORE the spool fallback (`[REQ-WAN-SPT-HOSTED-DELIVERY]`, leg 1); (2) **the ACTIVE→IDLE edge** — `drain_idle_window` (fired by `api state idle`) claims the pending spool (NON-DEFERRED only — see SCOPE below) and injects each via the same leg, reusing the hook-poll take/ack so a concurrent `api poll` cannot double-deliver (`[REQ-MSG-IDLE-EDGE-DRAIN]`, leg 2). The v0.14.3 LAW holds on BOTH triggers (7.22): no working binary ⇒ SPOOL LOUD (the row is released back intact), NEVER a raw PTY write. Hook-poll remains a valid third delivery trigger; it is simply no longer the ONLY one.
- **spt-core mapping:** impl `spt-daemon/src/inject.rs` `try_spt_hosted_inject`/`is_spt_hosted_no_relay` (the shared leg) called by `wan.rs` `receive_wan` (leg 1) AND `spt/src/api/delivery.rs` `drain_idle_window` (leg 2, via `spool::claim_idle_edge_at`/`release_at`); int (real broker+PTY+binary) `tests/inject_control_wedge.rs` `wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` (leg 1) + `spt/tests/idle_edge_drain_e2e.rs` `spool_while_active_then_idle_fires_injection` (leg 2) — both prove delivery with NO hook poll.
- **SCOPE (amended 2026-07-27, FIELD-TRUTH W1 roll-in, operator ruling):** this no-starvation guarantee binds the **`default` and `idle_only` classes ONLY**. The **`active_only` (deferred) class is explicitly OUTSIDE it** — per ADR-0028 it is hook-carried background context whose contract is "active hook window only; never wakes an idle agent", so with no hook cadence it WAITS. That is starvation *by design*, not a defect, and it is the one case where "reached the spool" does not imply "will be delivered without a hook". Leg 2 originally rescued the deferred class too; that rescue was REVOKED, because the rescue WAS the defect: doyle's spool take-audit on a live perch showed every boundary-spooled spt-shells shell-context row (`window=active_only`, `deferred=1`) taken with `taken_leg=idle-inject` ~200ms after spooling — each injection STARTING A TURN on an idle agent, the precise thing "never wakes" forbids. The idle-edge and parked-re-offer claims now exclude `deferred=1` unconditionally (no caller flag widens it — guard 4 of `[REQ-SEND-WINDOW-DRAIN-HONOR]`). **Diagnosis rule that follows:** an `active_only` row sitting pending on a hook-quiet endpoint is CORRECT behaviour — do not read it as this hazard recurring.
- **Source:** F-023 (BUILD-F023-WANIDLE; doyle RCA + gate, todlando build 2026-07-02). Delivered in two legs: WAN-ingress inject (leg 1) + idle-edge drain (leg 2). Scope narrowed 2026-07-27 (FIELD-TRUTH W1 roll-in) — see SCOPE above.
<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->

---

### 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 <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` (set at `cmd_endpoint_run` → `launch_harness_brokered_in` → `SpawnReq.adapter`) 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 via `split(':')` — TWO divergent matchers on ONE seam. **(D2, silent success):** the `affected.is_empty()` branch replied `KIND_APPLIED` and RETURNED WITHOUT SWAPPING. Once the CLI routes the apply to the daemon it has fully delegated — 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 reports old). `crc_swap` / the exe-lock / the terminate race were all EXONERATED (never reached — the swap was never attempted). Why `stop → update → relaunch` worked: a stopped endpoint fails the CLI live-gate → the direct `apply_release_crc_swap` path → a real swap.
- **Invariant:** (1) ONE shared parent-aware matcher `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option(session_adapter).0 == parent`) is used at EVERY live-update comparator — the CLI live-gate, the broker apply-filter, AND `select_endpoints_running_adapter`; NO exact `==` against a record name at any live-update seam (a profile-composite endpoint always resolves to its parent). (2) Once the apply is delegated the daemon owns the WHOLE apply: the CRC swap runs UNCONDITIONALLY (the terminate/restart loops simply no-op when nothing is resident), and `KIND_APPLIED` is reported ONLY after a real swap — there is no empty-affected early-return. A genuinely-exited endpoint therefore still swaps (correct: the on-disk install dir must reach the new bytes), and a `:profile` endpoint is coordinated exactly like a bare-name one.
- **spt-core mapping:** impl `spt-runtime/src/profile.rs` `adapter_parent_matches` wired at `cli.rs` `adapter_has_live_endpoint` + `broker.rs` `dispatch_adapter_apply` session filter + `broker.rs` `select_endpoints_running_adapter`; `broker.rs` `dispatch_adapter_apply` swaps unconditionally (empty-affected early-return removed). unit `profile.rs` `adapter_parent_matches_on_parent_not_composite` (matcher truth table) + `broker.rs` `select_endpoints_running_adapter_filters_dedups_sorts` (extended with a `:profile` composite row — RED under exact-match). int (folds this wave) e2e a/b/c: composite-session swap-LANDS + no-matching-session swap-STILL-runs + a different-adapter session UNTOUCHED.
- **Source:** F015B (operator escalation, doyle RCA code-verified end-to-end 2026-07-02; todlando build). ADR-0025 amendment records both invariants.
<!-- [doc->REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP] -->

---

### F-019 diagnosis lesson — confirm an adapter binary actually SPAWNED before behavioral diagnosis  `[REQ-INSTALL-11]`
- **Failure:** a bare/relative adapter-shipped program path — `[message-idle-translation-binary].path = "cc-spt-idle-translate"` — passed VERBATIM to the broker spawns via `Command::new` against the daemon's cwd/PATH, FAILS (not on PATH), and the spt-hosted session FAILS CLOSED to raw inject. The binary's `{text}{delay}{key:enter}{commit}` choreography never runs → idle messages are typed into the harness but **never submitted**. `build_translation` DOES log `TRANSLATION_SPAWN_FAILED:<path>:<err>` (broker.rs) — but it lands on the **DETACHED daemon's stderr, which nobody reads**, so it is "silent" in practice. This cost a multi-hour black-box hunt (byte encoding, win32-input-mode `?9001h`, bracketed-paste, focus, pacing — all moot; the binary simply never ran).
- **Invariant:** every adapter-manifest program path resolves against the adapter install dir BEFORE PATH (REQ-INSTALL-11, `resolve_program_in_dir`) — at the harness session spawn (`harnesshost::launch_harness_brokered_in`: the idle-translation binary AND the session program), the W3d live-update RESPAWN (`broker::read_translation_path`), and the notif command (`with_install_dir`, both the daemon `notif.rs` and the api `reporting.rs` render paths). **Diagnosis rule:** before any deep behavioral analysis of a translation/adapter binary, CONFIRM it actually spawned — a resident process exists, and a deployed path edit changes behavior. `TRANSLATION_SPAWN_FAILED` on daemon stderr is the signal today, but its visibility is poor (surfacing it on the perch / a louder channel is a candidate follow-up).
- **Source:** F-019 (v0.14.2); root confirmed on real claude-spt (perri + doyle). The raw-inject FALLBACK itself (degenerate no-binary floor) is unchanged by F-019 — its removal (message stays SPOOLED + poll-fed, never raw PTY inject) rides a separate ADR-0022 amendment.
<!-- [doc->REQ-INSTALL-11] -->

---

### 7.25 A perch PINNED to a DEAD session self-heals (dead-owner re-pin) instead of wedging forever; a LIVE-owner rotation still refuses  `[REQ-HAZARD-SESSION-PIN-WEDGE]`
- **Failure (F-024C/F-024D, ENLYZEAM field + clean-room repro 2026-07-02):** `authenticate()` (auth.rs) gates `api poll`/`state`/`boundary` on `proof.session_id == info.json.session_id`. If ONE boundary rotation is LOST — the departing session dies (crash / tab-close) or its `/clear`-era `boundary` call never lands — the perch stays PINNED to the dead sid, and every id-scoped hook call thereafter AUTH_REFUSES *including `boundary` itself* (the very call that would re-pin presents the NEW sid). A permanent, self-inflicted strand: `ready:false`, stale `.idle`, drains no-op, WAN/idle spool sleeps forever — and `AUTH_REFUSED` is stderr-only, so INSIDE a hook it is invisible. No self-heal existed.
- **Invariant:** `authenticate()` gains a DEAD-OWNER fallback — when the caller's sid MISMATCHES the pin **AND** the perch's recorded owner pid is DEAD (`proc::is_process_alive == false`), ACCEPT the caller's sid and RE-PIN (rotate `info.json.session_id` via the locked `mutate_info` RMW + a LOUD `SESSION_REPIN` stderr line). Same trust model as `establish_perch`'s conflict gate, which already allows a rebind exactly when `owner_alive == false` (an orphaned perch accepts a new LOCAL owner). This is **ADR-0032 LAYER 2 of 3**. A LIVE-owner mismatch (a `/clear`/`/compact` live-pid rotation — SAME process, new sid) STILL REFUSES: squat protection, deliberately NOT widened to live owners. The live-rotation contract is the adapter's (**layer 1**: persist + present the departed session's prior sid as `boundary` proof — `[REQ-BOUNDARY-ROTATION-CREDENTIAL]`, perri's `.sid` state-file pattern is the reference); a self-proving `parent_pid`-ancestry rotation is the design-true **layer 3** (parked: needs an ADR + Windows parent-spoof caveats). ADDITIVE to token auth — the existing token-auth recovery path is UNTOUCHED (the branch fires only on no/failed token AND sid mismatch AND owner dead).
- **spt-core mapping:** impl `spt/src/api/auth.rs` `authenticate` dead-owner-mismatch branch (`read_pid` → `proc::is_process_alive` → `mutate_info` rotate + `SESSION_REPIN` log); unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins` (AUTH_REFUSED → drains+re-pins, RED pre-fix) + `live_owner_mismatch_still_refuses` (the `/clear` live-rotation squat guard, unwidened) + `token_auth_path_unchanged` (correct token wins first, no rotation).
- **Source:** F-024C/F-024D (perri field RCA on ENLYZEAM + doyle code-confirm 2026-07-02; the wedge latches on `/clear` even in a CLEAN env — the env-corruption domino was sufficient but not necessary). COVERAGE: dead-owner re-pin rescues CRASHED/DEAD owners ONLY; live-pid rotation is correctly refused without departed-session proof. Cross-refs ADR-0032 (the 3-layer rotation-credential model) + `[REQ-BOUNDARY-ROTATION-CREDENTIAL]` (layer 1, the adapter/harness-contract half).
<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->

### 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 hooks (`fatal: cannot copy … File exists`, exit 128), AND the `git config` step collides on git's per-repo `config.lock` (`could not lock config file …: File exists`) — either failure returns a hard `io::Error` that strands the losing caller.
- **Invariant:** N concurrent `open_or_init` on ONE fresh dir ALL return `Ok`. The init is race-tolerant: `git init --bare` retries a transient collision with a growing backoff that desynchronizes racers (a lone re-init on a partially-copied dir completes cleanly — verified), treating a now-present `HEAD` as success (open-after-lose); the required `core.autocrlf=false` pin retries a transient `config.lock` collision (the value is idempotent, so the winner writes the same bytes). Convergence is guaranteed — once any racer creates `HEAD`, every other either sees it or re-inits alone. Non-lock / non-copy failures still fail fast.
- **spt-core mapping:** impl `spt-store/src/branchstore.rs` `open_or_init` → `init_bare_tolerant` (bounded init-collision backoff-retry + open-after-lose) + `config_set_locked_retry` (bounded `config.lock`-aware retry). unit `branchstore.rs` `concurrent_open_or_init_on_one_fresh_store_all_ok` (N=8 threads race one fresh dir → every result Ok, HEAD present, `core.autocrlf=false`; RED-FIRST: the pre-fix path failed the `git init` template-copy loser).
- **Source:** doyle F-025 wave Item 2 (the G3-gate pump.rs:442 flake root-caused as this TOCTOU). The red-first repro surfaced the race is BROADER than the ledgered `config.lock` — concurrent `git init --bare` itself collides on template-hook copy.
<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->

---

<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->
### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`
- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id)`), NEVER the controller-detach path (`detach_if`→`clear_controller`→`stamp_driven_by`, the ONLY stamp-clear). The exit-waiter had no stamp clear, so the OutputLog dropped with the controller slot occupied and `controlled` / `viewer_count` / (`driven_by` for a remote controller) latched in `info.json` forever — the picker + cross-node gossip then lie about control state indefinitely. Distinct from 7.15 (that heals a SESSIONLESS perch via the reconcile; this is a session that DIED with its stamps still set, cleared at the reap itself).
- **Invariant:** on session reap the broker clears the perch's control/viewer stamps — `OutputLog::stamp_reaped()` = `set_driven_by(None)` + `set_controlled(false)` + `set_viewer_count(0)` via the known endpoint id — so no teardown path leaves a stamp behind. The broker stays the SINGLE writer; the clear is unconditional (idempotent when already clear) and RACE-FREE — the session is dead, so there is no live controller to re-stamp concurrently (the same argument as 7.15's no-session leg). Best-effort like the sibling stampers (empty endpoint ⇒ nowhere to record). The residual ~1 min `ONLINE` tail after child death is the roster liveness-decay window (separate, lower priority) — but `CONTROLLED` no longer lies.
- **spt-core mapping:** impl `broker.rs` `OutputLog::stamp_reaped` + the exit-waiter reap call (before `sessions.remove`); int (real broker+PTY, no mocks) `crates/spt-daemon/tests/control_stamp_lifetime.rs` `reap_clears_control_and_viewer_stamps` (spawn hosted session → the live controller stamps `controlled=true` → latch `driven_by`+`viewer_count` on disk → KILL the child → the reap clears all three; RED-first verified: fix disabled leaves `driven_by=Some`, `controlled=true`, `viewer_count=Some(2)` past reap). Killing the child (not dropping the conn) isolates the reap leg — the detach path never runs.
- **Source:** F-026 W1 #2 (BUILD-F026-PICKERTRUTH; doyle triage + gate, todlando build 2026-07-03).

---

<!-- [doc->REQ-STORE-CONTEXT-BRANCH-FILL] -->
### 7.28 A relative manifest path resolves against the ENDPOINT, never the daemon  `[REQ-STORE-CONTEXT-BRANCH-FILL]`
- **Class rule:** a relative manifest path naming a filesystem location the ENDPOINT reads or writes MUST resolve against the endpoint's own cwd (its `info.json.cwd`, read at USE time), never the daemon's process cwd. A single daemon hosts many endpoints with different cwds, so a relative path frozen or read against the daemon cwd points at a directory no endpoint uses. Keys that resolve via `{cwd}` template substitution (`[digest].source`, `[history].locate_template` — `{cwd}` filled from `info.cwd`) or that the daemon fills with a computed ABSOLUTE path (`[message-idle-translation-binary].path` ← install-dir/PATH program resolution) are already endpoint-anchored and out of scope; the verbatim-`PathBuf::from` keys are the risk.
- **Failure (F-026 SI-1, box-wide, evidence-complete):** the two-tier context BranchStore (`tracked/.seed.git`) held ZERO `a-*`/`p-*` branches on HFENDULEAM despite months of live-agent use — 0 objects, unborn HEAD, no `projects/` worktree checkouts, no commune drop files anywhere (psyche context flowed via the SEPARATE spt-agent-storage sync, masking it). ROOT: `BrainLifecycle::with_config_in` used `manifest.session.commune_dir` / `signoff_dir` VERBATIM (`PathBuf::from`). claude-spt declares `commune_dir = ".claude"` (RELATIVE, documented "resolved per-endpoint against its cwd"), so `drop_dirs()` yielded a bare `.claude` that `ingest_drops` joined against the DAEMON's cwd (`drop_path.exists()==false` every pulse → `continue` → zero ingest → zero commit). Compounded: the frozen `project_id = project_id_for_dir(commune_dir.parent())` = `project_id_for_dir("")` = garbage. Every daemon test injected an ABSOLUTE tempdir `commune_dir`, so the relative leg was NEVER exercised (the use-it-like-a-human gap).
- **Invariant:** drop-dir resolution + project-id derivation happen AT INGEST TIME against the endpoint's fresh `info.cwd` (`pulse_tick`, not frozen at construction): absolute → as-is; relative + a recorded cwd → joined under it; relative + NO cwd → SKIPPED with a LOUD-once `DROP_INGEST_SKIP` diagnostic (never guessed, never the daemon cwd). Project-tier routing is owlery-gated — an owlery-internal anchor (psyche-host session) resolves its drops verbatim but routes NO `p-<project>` branch (empty project_id → `route_slices` skips `commit_project`; mirrors the picker's owlery exclusion); a non-owlery anchor derives `project_id` via the SAME `project_id_for_dir` the picker uses (category-key parity). Pure back-compat: absolute manifests unchanged, no wire change, no adapter republish (the manifest already documented the contract). SECONDARY (perri consumable, release-ping): `p-*` fills only if the commune body carries a `<project-context>` slice — claude-spt's authoring must emit one.
- **spt-core mapping:** impl `spt-daemon/src/lifecycle.rs` `resolve_endpoint_drop_dir` + `is_spt_internal` + `warn_no_cwd_once` + `pulse_tick` (reads `info.cwd`, resolves each drop dir, derives spt-internal-gated `project_id`) + `spt-live/src/ingest.rs` `route_slices` (empty-project_id skips the project tier); unit `resolve_endpoint_drop_dir_cases` + `spt_internal_decision_table` (lifecycle.rs) + `empty_project_id_skips_project_tier` (ingest.rs); int (real pulse + real store) `relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch` — a RELATIVE `.claude` + a real endpoint cwd → the drop is found and `a-<id>` + `p-<project_id_for_dir(cwd)>` commit (RED-first: verbatim-relative resolution → 0 ingested, store pristine).
- **Source:** F-026 SI-1 (BUILD-F026-PICKERTRUTH; doyle RCA-gate + design ruling, todlando build 2026-07-03). Hazard-class sweep confirmed commune_dir+signoff_dir the ONLY verbatim instances.

---

<!-- [doc->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE] -->
### 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), so `mutate_info` returns `NotFound` and every stamper swallows it (`let _`). The adapter's bind then writes `InfoJson::new` with `controlled:false` DEFAULT, and since the operator never re-attaches, no later EDGE ever re-stamps → the endpoint reads uncontrolled FOREVER while driven. The 7.15/7.27 edge model (stamp on become-controller / clear on detach/reap) has no re-assert for a stamp LOST to a not-yet-existing perch. (The #3 display fix reads `controlled` correctly — it was simply datum-starved on this creation path.)
- **Invariant:** the broker (SINGLE writer) re-asserts each live session's control/viewer stamps to session-table TRUTH, DIVERGENCE-GATED — read the perch's recorded `(driven_by, controlled, viewer_count)`, compare to the live truth (`controller_by()` / `has_controller()` / live `viewer_count`), and write ONLY on a real difference (no per-poll fsync storm). The re-assert rides the `KIND_SESSIONS` handler (the daemon reconcile + picker poll it on cadence), so a fresh perch CONVERGES within one reconcile-poll window after bind — a BOUNDED window, no new timer. Truth is SNAPSHOTTED under each log lock; the convergence writes run OFF the lock (the lock-across-effect discipline, 7.12/5.16). Event-on-input is INSUFFICIENT (an idle controlled session — the operator attached, not typing — never converges); the poll-driven re-assert covers it. Complements 7.27: 7.27 clears a stamp when its session dies (downward), 7.29 asserts a stamp while its session lives (upward) — together the stamp always equals broker truth.
- **Coverage boundary (who MAY clear vs who MUST NOT — cross-ref 7.15):** the convergence's `driven_by`-clear leg fires ONLY when `has_controller()==false` — the DEFINITIVE broker signal (it reads its OWN controller slot, not the ambiguous `controller_by`). That is why the broker MAY clear a stale `driven_by` where the BRAIN reconcile (7.15) MUST NOT: the brain only sees `controller_by==None`, which a live LOCAL controller also reads, so a brain clear would false-heal a genuinely-driven session. This leg is belt-and-suspenders over the clean-disconnect `detach_if`→`clear_controller` (which already zeroes `driven_by` on a clean drop). The convergence does NOT close the HALF-OPEN wedged-remote residual — an abandoned REMOTE controller whose conn stays OPEN (no FIN) keeps the broker's controller slot, so `has_controller()==true` and convergence correctly KEEPS the stamp (it cannot tell a wedged-open conn from a live one). That leg stays deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` (oracle = the D4c NetPresence disconnect → `clear_controller`, per the 2026-06-19 ruling) — **narrowed 2026-07-22 by the Leg B measurement recorded in 7.15: the DEAD-transport-no-FIN half heals on QUIC's own idle timeout (120ms clean / 65s torn, measured), so what is still deferred here is the ALIVE-but-WEDGED conn, where the transport never dies and convergence genuinely has no signal to read.**
- **spt-core mapping:** impl `broker.rs` `stamp_divergence` (pure gate) + `converge_perch_stamps` (off-lock, single-writer, skips a perch-less/corrupt endpoint) + `OutputLog::has_controller`/`live_viewer_count` (snapshot accessors) + the `KIND_SESSIONS` handler (snapshot-under-lock → converge-off-lock); unit `broker.rs` `stamp_divergence_gates_writes` (no-diff → no write; `controlled false→true` = the stamp-before-bind case; driven_by / viewer / both); int (real broker+PTY) `control_stamp_lifetime.rs` `converge_stamps_on_sessions_poll_after_late_bind` (spawn with NO perch → spawn stamp swallowed → LATE bind writes `controlled:false` → a `KIND_SESSIONS` poll converges `controlled=true`; RED-first: hook disabled leaves `controlled:false` forever).
- **Source:** F-026 W1-addendum (BUILD-F026-PICKERTRUTH; doyle stamp-gap dispatch + convergence ruling + KIND_SESSIONS-hook approval, todlando build 2026-07-03).

### 7.30 A Psyche failure of ANY shape must NEVER remove or alter the parent endpoint's ready/hosted state  `[REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]`
<!-- [doc->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION] -->
- **Failure (paid-for, field brick 2026-07-04, adapter v0.13.2):** the pre-F-030 model kept a **resident** Psyche process the daemon supervised, with residency machinery (`confirm_residency_or_unhost`) that **un-hosted the parent endpoint** when the resident child went missing. A bad adapter ship (v0.13.2) made the psyche shim exit on every turn; the residency machinery read that as a lost resident and **tore down the parent's hosted state — the ready marker was removed and never re-stamped**, so every `--force-native` delivery then gated `cli-gate-not-hosted` PERMANENTLY. A Psyche problem silently bricked the parent LiveAgent. (Companion churn class: 7.31.)
- **Invariant:** a Psyche is a **bounded per-event turn**, not a resident process — its liveness is that turns succeed, so there is no "resident is gone" signal to react to. A psyche turn failure of ANY shape (spawn fail, non-zero exit, timeout, wedge) stamps **psyche fields only** (`psyche_host_error` on the Self perch) and **NEVER** touches the parent endpoint's `status` / ready marker / hosted state; the endpoint stays online and deliverable while its Psyche is unwell. The teardown-on-psyche-trouble path is **deleted** with the residency machinery. `REQ-HAZARD-LIVEHOST-NONRESIDENT`'s spirit (a psyche that cannot work must be visible on the perch, hosting must not churn) transfers to the per-event failure budget (7.31); its traceable entry carries a SUPERSEDED pointer here (the LIVENESS-DECAY→SUPERSEDED pattern).
- **spt-core mapping:** the residency/reap DELETIONS in `spt_daemon::livehost` (`reconcile_once`'s stop-side no longer un-hosts on psyche trouble; `host_one` holds no resident child) + `spt_daemon::lifecycle::run_psyche_event_turn` (a turn failure stamps `psyche_host_error` only) + the `first_turn_psyche_context` non-empty guarantee (a zero-context fresh turn can't masquerade as continue → reseed churn). Conformance int = the hall-bf shape: multi-subnet home, live endpoint, a psyche turn that fails every fire → parent ready marker PRESENT + deliverable stays TRUE, endpoint NOT un-hosted, no rehost churn, `psyche_host_error` stamped (the wave's heart).
- **Source:** F-030 (psyche-ephemeral) — paid-for by the hall-bf churn (ordinal 6491+) and the adapter v0.13.2 bad-ship brick, 2026-07-04.

### 7.31 The Psyche failure budget must count REAL per-event attempts — a resident rate-guard is blind to per-event churn  `[REQ-HAZARD-THRASH-GUARD-BLIND]`
<!-- [doc->REQ-HAZARD-THRASH-GUARD-BLIND] -->
- **Failure (paid-for, field evidence 2026-07-04):** the pre-F-030 resident-hosting thrash guard keyed on ledger-rate boundaries that were NOT the real re-host attempts. On hall-bf a **~12/min** re-host churn ran completely **invisibly** — the guard never tripped, never stamped, never cooled down, because the boot records it looked at were not ledger boundaries the guard counted. A runaway failure loop looked healthy.
- **Invariant:** the failure budget counts **REAL per-event attempts** — every bounded per-event psyche turn feeds a consecutive-failure budget (default 3): N consecutive turn failures → `psyche_host_error` stamp + cooldown, reset on the next clean turn. Because each real attempt is one counted turn, the churn the resident rate-guard was blind to now counts **by construction** (no separate rate belt). A synthetic per-minute failure loop MUST trip the budget.
- **spt-core mapping:** the per-event turn-failure budget IS the guard — `spt_daemon::lifecycle` `note_turn_outcome` / `psyche_turn_strikes_exhausted` (every real turn attempt feeds it; stamps `psyche_host_error` on exhaustion WITHOUT de-stamping `status`/ready — see 7.30). unit = a synthetic consecutive-failure loop trips the budget + stamps, pure/no-sleep, with a budget→never-exhaust guard-revert as the RED-first control.
- **Source:** F-030 (psyche-ephemeral) — paid-for by hall-bf's ~12/min invisible re-host churn (evidence #6), 2026-07-04.

### 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]`
<!-- [doc->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` event found it "already in the target state", the pure table returned `None`, and the endpoint reported **NO_EDGE on a definitely-suspended endpoint** and could never be woken. The mirror defect: a `Suspend` on that same cold void perch faked an Active→Suspended edge and fired a spurious transition echo (a bounded LLM call) on a driver that was never active.
- **Invariant:** the effective resting state of a perch is derived through the ONE shared `resting::effective_rest_state(alive, unbound, intent)` — liveness discriminates warm from cold, stored intent refines only *within* warm, and absent intent NEVER defaults active. Both `advertised_status` (via a RestState→Status map) and `apply_event`'s `from` route through it, so the two readers can never drift. Any new rest-state reader MUST use the same derivation — two independent derivations WILL disagree, and that disagreement IS this bug.
- **spt-core mapping:** `spt_daemon::resting::effective_rest_state` is the single derivation; `advertised_status` and `apply_event` both delegate. unit = a pure 3×4 liveness×intent table + two `apply_event` wire tests (a cold void perch Wakes as a real Suspended→Active edge; a cold void Suspend is a no-edge that fires no echo), each RED-first against the old `unwrap_or(Active)`.
- **Source:** REMOTE-TRUTH A-1 (effective-instance-state shared derivation), ADR-0033 §Decision (Q1 ruling: one shared derivation).

### 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]`
<!-- [doc->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 non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`) and the inject floor (`[REQ-HAZARD-INJECT-WORKER-POISON]`) were fixed one lock at a time; the SURVIVING class was the attach-path locks — the session map (`sessions` ×18 + its `sessions_exit` alias), the per-session `OutputLog` ring (×11), and `pair_holds` (×4) — all bare `.lock().unwrap()`.
- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks the invariants and RESETS the ring empty (`next_seq` preserved — cursors never rewind) + loud-logs on violation. Fail-fast on the log would REINTRODUCE the wedge; blind recover would serve torn bytes; clamp-or-reset costs only scrollback that self-heals on the next PTY output + repaint. Any NEW broker-resident lock must use `recover`/`recover_log` or carry a documented fail-fast justification.
- **spt-core mapping:** `broker::recover` / `broker::recover_log` / `OutputLog::clamp_or_reset`. unit = a poisoned session-map recovers to a usable guard (the next attach still opens), a torn ring clamps/resets-empty only when torn (coherent untouched, `next_seq` preserved), and `recover_log` wires the clamp onto the poison branch — each RED-first against a bare `.lock().unwrap()` / an un-clamped `into_inner`. int (scripted panic-under-lock during concurrent attach → next attach opens) deferred to the two-host rig.
- **Source:** REMOTE-TRUTH B-1 (pivoted — the triage's named floor sites were already closed by `[REQ-HAZARD-INJECT-WORKER-POISON]`; the surviving attach-path lock class is the real root), doyle B-1 ruling (recover all three via one helper + the OutputLog ring coherence clamp).

### 7.34 A dead `rec.pid` on an spt-hosted perch is EXPECTED — no reader may alive-gate on `rec.pid` alone  `[REQ-HAZARD-DEAD-REC-PID]`
<!-- [doc->REQ-HAZARD-DEAD-REC-PID] -->
- **Failure (paid-for, field evidence — the F-026 #11 dead-pid class):** an spt-hosted endpoint's `rec.pid` records the ephemeral bind-CLI pid, which dies IMMEDIATELY after bind (the broker holds the PTY; there is no resident harness process at that pid). Readers that alive-gated on `rec.pid` alone treated the perch as stale: self-detect leg (c) could NEVER resolve an spt-hosted sender (its from-stamp degraded to `cli@NODE`, operator #7, and replies bounced `NO_PERCH` — sighted on hall-bf/ball-b, v0.24.0).
- **Invariant:** a dead `rec.pid` on an spt-hosted perch is an EXPECTED state, not staleness. NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed `status` field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution, where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (ADR-0021 seed-hint discipline: pid is a bind-time SEED, not a truth anchor — re-anchoring truth in `rec.pid` is overruled by design). Any newly sighted `rec.pid`-alive-gating reader gets the same scoped fix and extends THIS requirement's evidence — no new REQ per reader.
- **spt-core mapping:** `roster.rs` `detect_self_by_ancestry` (the first sighted reader, fixed by `[REQ-SELF-DETECT-PARENT-PID]`: `parent_pid` candidates alongside `rec.pid`). int = the E-1 red-first (`detect_self_resolves_spt_hosted_perch_via_parent_pid`: dead `rec.pid` + live-ancestor `parent_pid` resolves), dual-tagged as this class's test.
- **Source:** REMOTE-TRUTH E-1 rider (doyle ruling 2026-07-05: scoped reader-side (b) over re-stamping `rec.pid` (a) — ADR-0021 ground truth, migration hole, blast radius).

### 7.35 The cached ceremony-clock NTP offset must NOT survive an OS clock STEP — an offset measured against the pre-step clock strands every pairing for the TTL  `[REQ-HAZARD-CEREMONY-CLOCK-STEP]`
- **Failure (paid-for, field evidence — the enlyzeam BIGNET-join RCA 2026-07-06):** enlyzeam's `w32time` was STOPPED, so its system clock drifted +3m30s; the pairing ceremony offset (REQ-PAIR-8, `ntp.rs`) correctly corrected the TOTP clock — until an operator `w32tm /resync` STEPPED the OS clock back −210s under the LIVE daemon. The cached offset had been measured against the OLD (drifted) clock and its snapshot was monotonic-only (`when: Option<Instant>`), so the step was invisible to the cache: for up to the 15-min TTL the ceremony clock stayed ~7 TOTP steps in the past, every `subnet join` returned NO_SEED_HOLDER while fresh-process probes met in <500ms, and only a daemon bounce (which forced a fresh query) healed it. Timeline-proven: refresh cadence 15:08/15:23/15:38/15:53; resync-step 15:45:46; joins failed 15:46–15:52; bounce healed instantly.
- **Invariant:** the offset cache MUST detect an OS clock step and re-query, never apply a stale offset across it. The snapshot stores an `(Instant, SystemTime)` PAIR; on every read, if the wall clock diverged from the monotonic clock since the snapshot beyond a small tolerance (`|wall_elapsed − mono_elapsed| > ~2s`) the OS clock STEPPED ⇒ force an immediate re-query (offset recomputed against the new clock) regardless of TTL. Belt-and-braces at the join edge: `meet_seed_holder`, on search-deadline exhaustion, forces ONE fresh NTP query and runs one final sweep at the corrected step before the NO_SEED_HOLDER verdict — so a stepped-clock join self-heals WITHOUT a daemon bounce. The step tolerance sits far below one TOTP step (30 s) and well above scheduling jitter. Contract of REQ-PAIR-8 (lazy TTL cache, system-clock fallback, never sets the OS clock) is otherwise unchanged.
- **spt-core mapping:** `spt_net::net::pairing::ntp` — `CachedOffset{when, wall_at}` pair + `refresh_needed`/`clock_stepped`/`signed_secs` (step-detect) + `force_refresh`/`invalidate` (the meet-exhaustion re-query seam); `spt_daemon::pairhost::meet_seed_holder` via `sweep_then_final_retry` (force-refresh + one final sweep on exhaustion). unit = `clock_step_forces_refresh_via_injected_reads` (injected mono/wall reads: a wall step forward OR back with mono barely moved re-queries; agreeing clocks within TTL do not; TTL-expiry and never-queried also refresh — no 15-min sleep) + `invalidate_clears_the_snapshot` + `meet_retries_once_after_refresh_on_exhaustion`/`meet_final_retry_failing_surrenders_once` (exactly one post-refresh retry, then surrender with the richer error).
- **Source:** JOIN-TRUTH W1/D3 (doyle /diagnose enlyzeam RCA; the offset defeated by a live clock step is the D3 of four defects, D1 multihome + D2 loud-fail + D4 verbose-clock the siblings).

### 7.36 The broker control plane and PTY fan-out must NEVER block on a single subscriber connection — a suspended brain conn must not wedge control  `[REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]`
<!-- [doc->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE] -->
- **Failure (paid-for, rig-CONFIRMED 2026-07-06/07 — `NtSuspendProcess` on the brain, no update involved):** a controller's writer thread does a BLOCKING socket write to its brain subscriber conn. When that brain is suspended (or black-holed) the write never returns. The output-driven eviction path (`append` → `mark_controller_gone`, bounded by 7.12's `CONTROLLER_WRITE_DEADLINE`) only fires on NEW output, and `reap_dead_controller` (7.29 companion) only caught a writer that had EXITED — so a controller BLOCKED (not exited) on an IDLE session was evicted by NEITHER. Within seconds: attached output froze, `detach` did not release the control stamp (release routed through the wedged conn), reattach was REFUSED (`controlled-by` latched), `rc --take` hung, and `daemon status` still read healthy. Every brain cycle — including every `update apply` — has such a freeze window; a stalled or slow-draining NEW brain (6 sessions + psyches + WAN on the incident night) makes it a PERMANENT wedge until a bounce. `brain.ready ≠ subscribers drained` — why the 22:47 apply "promoted" while frozen.
- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stall-evicts it. Eviction is triggered TIME-based, not output-driven: at the take/reattach path (`resolve_subscribe`, so control self-heals against broker truth even on an idle session) AND at the `KIND_SESSIONS` reap. (2) The stamp release is BROKER-side — `clear_controller` re-stamps `driven_by`/`controlled` with no brain round-trip, so `controlled-by` can never latch behind a dead transit; `rc --take` and reattach-refuse consult live `has_controller()` truth. (3) The evict is OBSERVABLE, never a silent absence — a per-evict broker-log line plus a broker-global tally (`count` + `last_ms`) surfaced on `daemon status` (a brain that never returns is then a diagnosable fact). Idle ≠ wedged: an idle controller (writer parked on `rx.recv()`, `write_blocked_since` `None`) is NEVER evicted; only a genuinely-blocked writer is. Complements 7.12 (which bounds a merely SLOW controller's Full channel) and 7.29 (which converges stamps to broker truth) — this covers the BLOCKED-conn case both leave open. Deferred (Q2, minimal-plus): the WHOLESALE take/release verb-set migration off the brain stays out; the suspend-brain int rig is the arbiter — if it passes with the stall-evict shape, the wholesale move is scope creep. **Residual — BOUNDED by W2, CLOSED by W3 (`REQ-UPDATE-PROMOTE-DRAINED`):** the stall-evict shrank the update-apply FALSE-PROMOTE window from unbounded to `BRAIN_WRITE_DEADLINE` (15 s) — `brain.ready` could still arrive inside that window while blocked writes pended on the OLD-generation subscriber conn (`brain.ready ≠ subscribers drained`, the 22:47 apply). W3 CLOSES it: the ADR-0018 brain-trial promotion gate (`brainproc::run_trial`) now LATCHES ready-seen and promotes only on ready **AND** `TrialEnv::old_gen_drained()` — a broker-truth read (`Broker::any_local_controller_wedged`, the brain's own `by:None` conn wedged past the deadline; a remote conn is excluded, it is the W2 concern not an old-gen brain conn), no brain round-trip. Ready-but-never-drained elapses the trial window → kill + rollback (conservative; never a false-promote onto a wedge). The false-promote int rig (`tests/false_promote.rs`) exercises the promotion path itself, RED-first.
- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (`KIND_STALL_EVICTS` IPC); `brain.rs` `Brain::stall_evicts`; `cli.rs` `render_stall_evict_line` (daemon-status surface, public wording). unit = `controller_write_stall_predicate_distinguishes_wedged_from_idle` + `stalled_incumbent_is_evicted_on_reattach_and_control_released` + `reap_evicts_a_blocked_writer_not_only_an_exited_one` + `stall_evict_line_surfaces_only_a_real_tally`. int = `brain_decouple.rs` `suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks` — the deterministic in-process ANALOG of a suspended brain (a REMOTE controller that stops reading parks the broker writer in `write_frame`, the same primitive `NtSuspendProcess` produces), asserting EXACTLY four things: (1) an attached viewer keeps ticking during the wedge, (2) a second operator's `Control` subscribe COMPLETES via stall-evict (RED-first: remove the `resolve_subscribe` evict → `BusyControlled` + `evicts=0`), (3) it resolves promptly (well under the watchdog), (4) the evict is tallied (observability); Linux leg on kitsubito. It does NOT re-assert resume + cursor-replay byte-identity — W2's delta does not touch the replay path, so output integrity stays covered by the pre-existing ring/cursor units (effect-journal replay dedup, the `delivered_through` no-rewind CAS `advanced_cursor`, clean-repaint cold-attach). The live `NtSuspendProcess`/`SIGSTOP` run on a real brain child remains the MANUAL field rig from the incident night, not a CI test.
- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.

### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`
<!-- [doc->REQ-HAZARD-INJECT-SETTLE-REARM] -->
- **Failure (paid-for, field-CONFIRMED on 0.29.0 — doyle diagnosis + perri screenshot `WindowsTerminal_6iSjya8pMt.png`):** the shipped W5-A settle-gate (7-of-W5, `REQ-INJECT-MULTILINE-INTEGRITY`) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the premise that the head-swallow race is a STARTUP condition (the input reader not yet attached after spawn). That premise is FALSE: a mid-session `/clear` re-enters the harness's raw-mode input reader, re-attaching it and RE-CREATING the pre-settle window — but the one-shot had already fired at spawn, so `settle_before_inject` was SKIPPED and the head was eaten again. A checkpoint-wake payload injected right after `/clear` arrived head-truncated (mid-path `spt/Cargo.toml)`); echo-verify (Layer 2) is a default-OFF declared capability (`SPT_INJECT_VERIFY_ECHO`) for that session, so the loss was silent AND unrecoverable (a live-SENT inject leaves no spool copy).
- **Invariant:** the settle-gate re-arms before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY — the bug-prone class, where re-settling is cheap (a tick or two once the reader answers) and necessary (a mid-session reader reattach must be re-confirmed). The steady-state settle is latch-skipped ONLY where the probe is known UNOBSERVABLE on this PTY (a non-echoing ConPTY whose DSR answer never surfaces): there is no reader-reattach race to guard, and each settle would burn the full `INJECT_SETTLE` deadline. The class is discriminated by the settle's OWN return value — `settle_before_inject` returns `true` iff the session ring advanced (probe observed) — latched on the FIRST attempt only; a RE-DRIVE (`attempt > 1`, reached only on a swallowed head) ALWAYS settles regardless of the latch. This preserves the old one-shot's sole legitimate purpose (sparing a non-echoing ConPTY the deadline on every delivery) while closing the mid-session recurrence for echoing PTYs.
- **spt-core mapping:** `broker.rs` `run_inject_worker` — the `settled_once: bool` one-shot is replaced by a `probe_unobservable: bool` latch (init `false` = assume observable → settle first delivery); the gate is the pure `should_settle(attempt, probe_unobservable) = attempt > 1 || !probe_unobservable`, and the first-attempt settle's `observed` return sets `probe_unobservable = !observed`. `settle_before_inject` gains a `bool` return (observed vs timed-out). unit = `should_settle_rearms_on_observable_pty` (observable PTY re-settles each delivery; unobservable-probe PTY skips the steady-state settle; a re-drive settles in both classes — RED-first: restore the one-shot and the observable-PTY assert flips). No int edit: the `inject_control_wedge.rs` mocks (`findstr`/`cat`) never answer the DSR probe → the settle times out → `probe_unobservable` latches true after delivery 1 → behaviour is IDENTICAL to the old one-shot for those non-echoing mocks; only a genuinely echoing PTY changes (it re-settles), which is the fix. Rides `REQ-INJECT-MULTILINE-INTEGRITY`'s existing int coverage.
- **Source:** post-0.29.0 field fix (doyle diagnosis + design ruling `BUILD-SPEC-inject-settle-rearm`; todlando build 2026-07-08). An impl bug in the shipped W5-A Layer-1 fix, not a new invariant class — the settle-gate was correct but under-armed.

### 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]`
<!-- [doc->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<Mutex<SendHalf>>` 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_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefinitely** (~127.95 s in the capture, released only when a brain restart tore the connections down). The 7.36 logical `stall_evict_controller` removed the controller ROLE but neither canceled the in-flight pipe write, closed the physical connection, nor called `CancelIoEx` — the detached writer kept its `SharedSend` clone and live stack-owned mutex guard, and every other write queued at that gate wedged behind it. Load-gated: needs a real `seq>0` frame plus a non-draining consumer (seq-0 boot conns and 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. Neither existing deadline bounds 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.
- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On deadline / partial write / cancellation / unknown completion: (1) poison the whole physical conn idempotently, (2) abort read+write so `handle_conn` reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer, (4) NEVER reuse the conn (a timed-out length-prefixed frame may be partially written — reuse would corrupt framing), (5) finish/join the retired writer before reporting physical cleanup. Controller (replay + live), viewer, dispatch-reply, and nethost stream/presence writes ALL route through the primitive — any raw unbounded write left behind the gate preserves the failure class. NO additional output queue (the bounded queue + isolated writer already exist; the block is BELOW them); NO `PIPE_NOWAIT` (recorded mid-frame corruption risk). The logical stall-evict (7.36) stays as the ROLE-release trigger; physical retirement authority is the bounded write itself. Unix keeps its existing semantics under the same poison/retire invariant.
- **spt-core mapping:** `spt-daemon/src/conn.rs` `BrokerConn` (Condvar-gated serialized write gate + poisoned state + per-in-flight op id + watchdog + `abort_physical()`: cfg(windows) `CancelIoEx` → completion handshake → `DisconnectNamedPipe` raw kernel32 externs; cfg(unix) `UnixStream::shutdown(Both)`); `broker.rs` `SharedSend = Arc<BrokerConn>` + `controller_writer`/`viewer_writer`/`send_frame`/`send_error` routed; `nethost.rs` stream-log/presence sends routed. int = `brain_decouple.rs` `non_draining_controller_stall_evict_releases_writer_and_connection` (`#[cfg(windows)]`, the hertz 9-step: real PTY burst, black-holed remote controller, exactly one logical stall-evict via an independent draining conn, the black-holed conn's `handle_conn` finishes ≤2 s of logical release WITHOUT dropping the client, a write on the old client conn fails, a fresh controller resumes from the frozen cursor) — RED-first pre-fix; the cross-platform companion `suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks` stays green on both OSes.
- **Source:** UPDATE-WEDGE round-4 (hertz root doc `docs/UPDATE-WEDGE-ROOT-CAUSE.md` @f8596ca; doyle gate ruling `docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md`; todlando build 2026-07-09). The residual 7.36 left open: logical eviction without physical retirement.

---

### 7.39 Per-session identity env (`SPT_ENDPOINT_ID`/`OWL_SESSION_ID`/`SPT_AGENT_ID`) is NEVER inherited — the daemon scrubs it at startup AND on every role spawn, regardless of any role's declared `env_remove`  `[REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]`
<!-- [doc->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE] -->
- **Failure (paid-for, perri field RCA 2026-07-09/10 — F-036 psyche seat-theft):** a daemon restarted from inside an agent session (routine during core dev / `spt update apply`) carried the session's `SPT_ENDPOINT_ID=doyle` and passed it verbatim into every `[session.psyche_resume]` spawn — core stripped only each role's DECLARED `env_remove` list (runtime.rs `command_for`), so ONE adapter `env_remove` miss infected the whole node. Every psyche claude turn fired SessionStart, the adapter hook saw the leaked endpoint id, took the bind path, and ROTATED the victim's perch onto the psyche's own custody sid with a valid prior-sid proof — every pulse. Field blast radius: lia/deployah/doyle psyches ALL briefed as `<sptc-active-perch id=doyle>`; 37 peer msgs drained into lia's psyche transcript; victim deliveries eaten, communes dark, sends downgraded to `from:cli@node`.
- **Invariant:** the identity set (`spt_runtime::IDENTITY_ENV_VARS`) is per-session state and NEVER correct inherited state for a daemon or any child it spawns. The daemon (broker AND brain entries) scrubs its own process env first thing (`scrub_identity_env`); the shared role-spawn command builder scrubs the set unconditionally and LAST (after `env_remove`, read-env stamps, and the recursion guard), so no adapter declaration — missing or pathological — can leak identity into a role child. A spawn that NEEDS an identity var (a hosted harness) receives it by explicit per-spawn injection (manifest `[env] direction = "inject"`), never by inheritance. The same startup scrub also clears the inject-echo knobs (`spt_runtime::INJECT_ECHO_ENV_VARS`: `SPT_INJECT_VERIFY_ECHO`/`SPT_INJECT_FORCE_ECHO_MISS`) — same inheritance class, different payload: an inherited dev-shell export force-enabled the default-OFF Layer-2 echo-verify host-wide, and a false verify-miss RETYPED whole sequences into a live session's input field (the F-033 typed-garbage symptom). These knobs are startup-scrub ONLY (not in the role-spawn builder scrub): explicit per-spawn declaration stays the production on-switch. Companion guard (F-036 leg c, `REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD`): a bind whose sid equals a nested psyche's OWN custody sid is refused loud (`PSYCHE_CUSTODY_SQUAT`) — the seat-theft class stays unreachable even under an unknown future leak vector. Related docs fix (leg b): `recursion_guard_env` is honored for ANY role declaring the field — the schema description is role-agnostic, not "summarizer children".
- **spt-core mapping:** `spt-runtime/src/runtime.rs` `IDENTITY_ENV_VARS` + `scrub_identity_env` + the `command_for` unconditional scrub; `spt-daemon/src/daemon.rs` `Daemon::run` / `Daemon::run_brain` startup scrub; `spt/src/api/startup.rs` `establish_perch` custody-squat refusal over `spt-store/src/psyche_custody.rs` `custody_squatter`.
- **Source:** MSG-IDENTITY W1 (perri F-036 RCA; doyle dispatch `docs/MSG-IDENTITY-DISPATCH.md`; adapter half shipped claude-spt v0.18.8 — this is the core-layer defense so no adapter miss can ever leak identity again).

---

### 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]`
<!-- [doc->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 `<project-context>` slice was parsed but never committed, yet the source drop was still deleted → the project-context content permanently lost. perri's two-sliced echo-commune INGESTED (file deleted) yet never surfaced at her next SessionStart resume-pull; adapter exonerated. Legacy spt held commit-first-then-delete parity; the modern two-slice ingest broke it.
- **Invariant:** delete-after-commit, per slice. Distinguish a write **SUPPRESSED by precedence** (incoming older than durable → already superseded → safe to delete) from a slice **un-committable now** (empty `project_id`, or any write error → the `?` retry path). An un-committable non-empty project slice is preserved as a **project-only pending slice under the COMMUNE suffix, whatever the source drop's kind** — a signoff-suffix pending would be reaped by `sweep_stale_signoff` on the next listener start (the signoff sentinel is write-once-per-generation), recreating the black-hole on the signoff path; the commune-named pending is a pure project-slice carrier both suffixes ingest identically and the sweep never sees. The already-committed live slice is dropped from the pending so retries never re-commit it (no per-tick branch-commit churn; `write_context` has no LLM-after-LLM suppression), the rewrite is idempotent (the pending form re-parses to itself), and the next ingest with a resolvable `project_id` commits + deletes. A newer commune from the mind overwriting the pending file is a supersede (fresher LLM snapshot), not a loss. An endpoint whose project id NEVER resolves (psyche-host anchor cwd) retains its pending indefinitely — an intentional bounded no-op per pass, preferred over any deletion heuristic. Loud `COMMUNE_PROJECT_DEFERRED` on the preserve transition only.
- **spt-core mapping:** `spt-live/src/ingest.rs` `ingest_drops` (deferred-project preserve + `Ingested.preserved`); `route_slices` project gate unchanged (`REQ-STORE-CONTEXT-BRANCH-FILL`); kin `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (core stays the single writer — the preserve rewrite is core's own write).
- **Source:** MSG-IDENTITY W3 (doyle triage, code-grounded ingest.rs:156 gate vs :200 delete; repro fixture F-032-commune-2026-07-08T222721Z.md).

---

### 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]`
<!-- [doc->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 fresh dispatcher replays FINISHED historical attach streams. A replayed historical Attach calls `attach_as(Control, same origin)` → same-identity silent `become_controller` (no `Displaced`) steals the CURRENT controller; the replayed EOF then `detach_session` CLEARS it. The live serve loop still believes it is Controller but its sink is deselected — the operator sees a FROZEN remote PTY (detach + fresh `spt rc` recovers, no PTY restart needed). Adjacent leg: classification peeks ring seq 0, but the data ring is bounded (4096 chunks) — an active stream whose opener evicted classifies Unknown/Failed with the claim burned pre-spawn, never retried → active stream permanently abandoned.
- **Invariant:** redispatch eligibility is LIFECYCLE-gated: a finished/terminal stream row is retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach); classification identity is restart-durable and independent of the evictable data ring (immutable opener fact pinned per stream until close); claims are retryable on transient worker-setup failure and terminal on terminal outcomes (no hot-loop). The legitimate same-`by` successor re-take after a brain restart still silently re-takes — the discriminator is lifecycle, never origin identity. Regressions ride the PRODUCTION rediscovery path (no manual re-serve — the pre-fix e2e bypassed exactly the broken seam).
- **spt-core mapping:** `spt-daemon/src/dispatch.rs` `run_dispatch_loop` claim condition + `peek_first_line` classification + worker spawn; `spt-daemon/src/nethost.rs` `NetShared.streams` insert/finish + `DEFAULT_STREAM_RING_CHUNKS`; `spt-daemon/src/applyhost.rs` (the brain-cycle trigger).
- **Source:** REDISPATCH-TRUTH W1 (hertz source-level RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`, doyle seam-verified; ADR-0038; distinct from the CLOSED 0.30.x session-cursor resume-steal saga).

---

### 7.42 A node holding a valid roster address for a peer is NEVER route-less — a failed dial must not delete the only bootstrap route  `[REQ-HAZARD-MESH-BOOTSTRAP-TRAP]`
<!-- [doc->REQ-HAZARD-MESH-BOOTSTRAP-TRAP] -->
- **Failure (paid-for, hertz field RCA 2026-07-10 — HFENDULEAM + ENLYZEAM fully sequestered from every subnet member, symmetric, green-status):** the pump resolved dial addresses from the exact `peer-addrs.json` entry else id-only discovery — never the valid `RosterEntry.address` — and every `PRESENCE_DIAL_FAILED` unconditionally `drop_seed`'d the cached entry, while the cache refilled only after a future successful seed-proof connection. One transient failure converted a warm route into id-only-or-nothing; with id-only discovery stalled, isolation prevented its own repair. Both rosters held correct direct addresses (Tailscale ping ~12ms) the entire time; `net_up: true`, fresh heartbeat, normal durable counts — every status surface answered a different question than "can I reach anyone?". Compounding: `PeerAddrStore::put` validated nothing, so persisted poison rows (outer key ≠ nested `address.id`) survived roster correction on both nodes.
- **Invariant:** dial-address resolution is the ROUTE CHAIN — exact cache → validated `RosterEntry.address` (id-match required) → id-only discovery — always fully consulted in order. A failed dial demotes a cached route (suspect), never deletes a sole route; removal happens only via validated-fresher replacement or roster tombstone. Validated roster addresses reconcile into the cache at startup and on roster merge — recovery never requires an already-successful connection or operator state surgery. `outer key == address.id` is enforced on load and write (repair-from-roster or reject, loudly; migration never bare-deletes the file). Peer-failure telemetry is stage-split and stamped; health reports live peer count + last real progress, and the incident's exact fingerprint renders DEGRADED.
- **spt-core mapping:** `spt-daemon/src/pump/mod.rs` `resolve_submit_addr` + `presence_state_effect` (`PRESENCE_DIAL_FAILED` arm); `spt-store/src/peeraddrs.rs` `PeerAddrStore::put`/`drop_seed`; `spt-daemon/src/seedproofx.rs` `gapfill_peeraddrs`; status renders (`daemon status` / `subnet status`).
- **Second clause (added 2026-07-31, see 7.59):** a peer is never stranded by an address that has ALREADY failed either. Preferring the roster address assumed the retained row was fresher than the failed one; the cross-site incident falsified that assumption (7.60 froze every self row), turning this entry's fix into a `fail → demote → reinstall-same → fail` livelock. The clause is ADDITIVE — everything above still holds, and a roster address DIFFERING from the failed one still resolves and still supersedes a suspect row.
- **Source:** MESH-RECOVERY W1 (hertz RCA `.claude/reports/2026-07-10-hertz-session/02-mesh-isolation-rca.md`, doyle seam-verified 2026-07-16; ADR-0039 — amends REQ-CONV-1's falsified "a stale addr never strands a peer" drop-on-fail mechanism).

---

### 7.43 One wedged stream subscriber must NEVER stall stream serving — replay halts at the first failed write, a poisoned subscriber is removed, and recovery machinery must not manufacture new replay victims  `[REQ-HAZARD-REDISPATCH-STALL]`
<!-- [doc->REQ-HAZARD-REDISPATCH-STALL] -->
- **Failure (paid-for, hertz field RCA 2026-07-16 — live v0.34 boxes, recurrent 20–30s PTY/RC freezes, DISPATCH tails 17–62s):** a COMPOSITION, not one new timer. The dispatcher's retryable claims (500ms/1s ×3) re-drove the opener fallback whose guard arm is a broad `Err(_)` (`dispatch.rs:414` — comment intends old-broker-only; catches transport timeout/EOF/poison), each pass installing a throwaway peek subscriber. `StreamLog::attach` synchronously replays the ENTIRE retained ring under the per-stream mutex, `let _ = sub.write(&frame)` discards errors, iteration continues past failure, and the poisoned subscriber STAYS INSTALLED — one wedged subscriber conn = serial 15s bounded-write poison windows (33 observed, all 15,000–15,154 ms — the round-4 SharedSend deadline firing back-to-back), composing with unbounded Whole-brain reads + a distinct unbounded wire conn + the NetHost 10s send into the observed tails. 4361 stream-sub-attach records per brain generation. Per-stream, not broker-wide (unrelated conns progressed mid-poison).
- **Invariant:** subscriber I/O never holds the `StreamLog` mutex (enqueue-under-lock, I/O outside, bounded per-subscriber queue; overflow = detach + resume-from-cursor); replay and fan-out halt at the FIRST failed subscriber write and the failed subscriber is REMOVED (attach + append + finish, all sites, `PresenceLog` mirrors included); the ring-peek fallback fires ONLY on the explicit unsupported-verb/old-broker answer; a deadline-poisoned replay is circuit-broken (global backoff, no reinstall until the prior subscriber is fully gone — an upgrade of bounded retry, never a revert to burn-the-claim abandonment); attach/detach validate ownership/generation so a stale worker never detaches a replacement controller (covers the unfinished-stale steal shape that finished-row retirement cannot see).
- **spt-core mapping:** `spt-daemon/src/nethost.rs` `StreamLog::attach`/`push`/`finish` + `PresenceLog` mirrors + serve/wire worker pairing; `spt-daemon/src/dispatch.rs` `first_line` fallback arm + claim retry/backoff + worker spawn; SharedSend bounded-write deadline (round-4, KH 7.7 kin).
- **Source:** REDISPATCH-STALL W1 (hertz v0.34 field RCA 2026-07-16, doyle seam-verified same day; ADR-0038 Amendment 2026-07-16; `.claude/reports/2026-07-16-redispatch-stall/rca-summary.md`; distinct from `update --restart`'s bounded 30s rc reconnect loop).

### 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]`
<!-- [doc->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`) → sender history unbounded. `stream_infos` filters `retired` only → every dispatcher poll serializes O(history) rows over IPC. `serve_registry_feed` ran `write_snapshots` (full rewrite of every subnet registry + heard.meta) up to TWICE PER 64KiB CHUNK, synchronously in the brain's event loop → brain stops draining IPC → broker seat writer blocks → 15s SharedSend bound → CONN_WRITE_POISONED (12→15 observed, family=Registry, fresh carrier each) → replay from seq 0 repeats the amplification. And seats retain FAMILY-AGNOSTICALLY: the conn loop accretes `my_stream_subs`, releases only at conn-loop exit, NO unsubscribe verb existed → ~546 of 589 broker threads were unnamed parked SubscriberSeat writers on completed Registry/sync/update seats. Independently: the brain re-hashed its 30.64 MiB exe every 500ms heartbeat (`write_ready` → `current_exe_hash`, a dropped W4 rider) = 15.5%/core + a breadcrumb that lies post-swap (path-read ≠ resident bytes).
- **Invariant:** a one-way (fire-and-forget) family's row is TERMINAL at successful FIN — the sender retires it; eligibility filtering happens server-side before IPC serialization; feed application is transactional per feed (snapshot writes O(feeds), never O(chunks × record-kinds) — no synchronous full-state rewrite inside a per-chunk drain iteration); a deadline-poisoned one-way replay retires terminal at a per-stream strike budget (safe: the next pump round re-advertises); every completed serve releases its SubscriberSeat (unsubscribe verb — thread joined, cursor dropped), all families; the exe self-hash is captured once per brain process (that IS the resident-bytes truthfulness contract).
- **spt-core mapping:** `spt-daemon/src/pump/mod.rs` `push_feed` (sender retire); `spt-daemon/src/nethost.rs` `stream_infos` (server filter) + SubscriberSeat teardown + thread naming; `spt-daemon/src/dispatch.rs` `serve_registry_feed` (transactional apply) + worker completion unsubscribe + strike budget; `spt-daemon/src/registryhost.rs` `write_snapshots` call sites; `spt-daemon/src/brainproc.rs` `write_ready`/`current_exe_hash`.
- **Source:** REGISTRY-LIFECYCLE W1 (hertz RCA `.claude/reports/2026-07-17-registry-stall-rca/README.md`, doyle-verified all legs 2026-07-17; ADR-0040; supersedes the v0.36 clean-window general-health ruling — the Attach-family fix stands, the class recurred for Registry).

### 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]`
<!-- [doc->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=true`, readers trusted `status=online` past dead-PID rejection). (b) Definitive session death stamped `status=offline` only, never `rest_state` → preserved `Active` intent = outstanding wake order → zombie WAKE_RESUME loops across generations (perri field). (c) `KIND_SESSIONS` poll snapshot raced the exit-waiter reap → stale poll relatched `controlled=true` on a dead session. (d) `--create` silently discarded; broker deduped every labeled SpawnReq into `Spawned(existing)` with no disposition → CLI printed success for a spawn that never happened; dup-guard and shutdown state machine consulted DIFFERENT liveness sources and contradicted on the same zombie (ALREADY_LIVE / OFFLINE / NO_EDGE three-way).
- **Invariant:** online is stamped only from persisted state + hosting authority; legacy rows self-heal after a SUCCESSFUL broker query (broker failure ≠ empty set); control-stamp cleanup runs for every endpoint absent from session truth regardless of state/controllability, split from offline classification; definitive death = ONE atomic store mutation (offline + rest_state=suspended + clear dormant_since_ms, same info.json write) — never a rest-event edge, never reader-side inference; stamp writes are session/generation-validated so a pre-reap snapshot can't overwrite post-reap truth; fresh-create is a broker-atomic policy with a TYPED conflict (distinct wire kind, loud on N-1), never `Spawned(existing)`; cycle verbs share one liveness authority.
- **spt-core mapping:** `spt/src/api/startup.rs` `cmd_listen` creator gate; `spt-daemon/src/livehost.rs` reconcile split + decide_resume; `spt-daemon/src/lifecycle.rs` `mark_offline` → terminal-normalize; `spt/src/cli.rs` `cmd_stop`/create dispatch; `spt-daemon/src/broker.rs` SpawnReq policy split + KIND_SPAWN_FRESH + dup-guard probe; KH 7.12/7.27/7.29 kin.
- **Source:** REGISTRY-LIFECYCLE W2/W3 (hertz emphasys triple + rest-normalize + spawn-fresh reports 2026-07-16, doyle seam-verified same day; ADR-0041; operator deployah-wedge field evidence).

### 7.46 An rc surface answers from live session authority, never a stale persisted projection — and a resuming perch is UNBOUND, not offline  `[REQ-HAZARD-RC-ATTACH-TRUTH]`
<!-- [doc->REQ-HAZARD-RC-ATTACH-TRUTH] -->
- **Failure (paid-for, hertz perri contradiction RCA 2026-07-17/18 + operator field recovery):** the broker hosted an honest live session (client tree alive, `SessionProbe::has_live_session_honest` = true) while the persisted perch row said `offline` — the resumed harness sat wedged at a failed native resume, SessionStart never fired, the perch never bound, inbound messages black-holed. `spt rc` trusted the offline row and refused pre-IPC while `endpoint run --resume` reattached to the very same session (authority split, decisive A/B). The offline fast-fail's own doc-comment invariant ("`offline` never co-occurs with a live session") was falsified in the field; the "resume gets no UNBOUND stamp — accepted" boundary note assumed the pre-bind window is transient when it can be permanent. Separately: rc mapped a harness-only refusal to stale-row guess copy, and leaked the user's qualified target string onto the wire as the endpoint id (false refusal after a correct dial).
- **Invariant:** every rc-family reader consults the single honest-session authority (ADR-0041 `SessionProbe`) before any persisted-status fast-fail — honest session ⇒ attach; no session ⇒ refuse; dead client tree ⇒ refuse/reap, never attach. UNBOUND = broker session exists + harness not bound, fresh/resume-invariant: resume stamps an existing offline perch UNBOUND (generation-safe rollback on spawn-fail/session death), bind owns UNBOUND→ONLINE. Refusal copy states known facts (harness-hosted ≠ stale row). The wire always carries the canonical bare endpoint id.
- **spt-core mapping:** `spt/src/rc.rs` `run_attach` offline fast-fail + `establish_attach` target carry; `spt/src/rc.rs` `SessionProbe`; the resume-launch spawn path (UNBOUND stamp + rollback); `spt/src/cli.rs` `cmd_endpoint_run` reattach arm (the already-correct reader, parity source).
- **Source:** RC-RENDER-TRUTH W1 (hertz RCAs 2026-07-16..18, doyle seam-verified; ADR-0042; organic pre-edit evidence preserved at hertz's `perri-info.pre-unbound-manual.json`).

### 7.47 The physical terminal is never mutated or terminated outside its renderer's ordered state model — output before exit, owned baselines, unconditional teardown  `[REQ-HAZARD-RENDER-LIFECYCLE]`
<!-- [doc->REQ-HAZARD-RENDER-LIFECYCLE] -->
- **Failure (paid-for, hertz stale-glyphs RCA 2026-07-18, all legs doyle seam-verified):** four render-lifecycle defects presenting as "missing whitespace"/stale glyphs. (a) The broker exit waiter direct-writes `KIND_EXIT` via `all_sinks()` while PTY output queues through `controller_writer` — the per-conn gate serializes bytes but not producer order, so Exit overtakes the child's final erase/SGR-reset/`?1049l` and rc returns immediately (the broker test suite already documented Exit-before-Output and compensated by draining; production rc did not). (b) `RawGuard::drop` restores raw mode/mouse/console mode but no VT display state — every exit path can leave alt-screen/cursor/SGR/scroll-region dirty. (c) The picker ran purge inline under the live ratatui alternate screen while the purge core wrote stderr — mutating the physical screen behind ratatui's previous-Buffer diff baseline, so later draws skip "already blank" cells and stderr fragments persist. (d) `render_repaint` omitted tracked DECSTBM margins, so client and server grids scrolled against different regions after a cold repaint.
- **Invariant:** one FIFO sequencer per attach sink — Exit is enqueued behind all prior Output (drain completion first); rc display teardown is a separate RAII guard from input teardown, unconditional and idempotent on every exit path including unwind (SGR reset, scroll-region reset, cursor show, leave alt screen, clear+home — while VT processing is enabled, before mode restore, before prose); a TUI surface has exactly one renderer — core verbs invoked from a live TUI return structured outcomes and write nothing to the terminal; cold repaint replays every tracked render-affecting mode (DECSTBM minimum). Blanks are real cells; a baseline is invalidated by any out-of-band mutation, reconnect, or resize.
- **spt-core mapping:** `spt-daemon/src/broker.rs` exit waiter + `controller_writer`; `spt/src/rc.rs` `RawGuard` (split guard) + reconnect give-up path; `spt/src/picker/mod.rs` purge arm + `spt/src/cli.rs` `cmd_endpoint_purge` output; `spt-term/src/screen.rs` `render_repaint` + DECSTBM tracking.
- **Source:** RC-RENDER-TRUTH W3 (hertz RCA 2026-07-18; ADR-0043; herdr v0.7.4 surveyed as prior art, ideas only — AGPL).

### 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]`
<!-- [doc->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 stream close (the only loud branch is different-remote+Take). `dispatch_input` is session-addressed with no controller validation, so A kept typing into the PTY while B exclusively received output — a split-brain: input-capable-but-blind incumbent, rendering-but-shared-control taker. `brain.rs` had already documented the `None==None` shape of the defect.
- **Invariant:** each rc invocation/attach mints a unique controller LEASE id carried through subscribe, the controller slot, and Input/Resize; the only silent successor is same-lease + equal-or-newer generation (dispatcher recovery, ADR-0038 fix-6 preserved); explicit Take on a DISTINCT lease always revokes ATOMICALLY and authoritatively (old lease fenced + old attach stream FORCE-closed; the Displaced notice via the old writer is best-effort — a Full-queue notice drop must never leave the incumbent installed or interactive) regardless of node match; the broker fences Input/Resize to the active lease — stale-lease commands are dropped after replacement (the invariant holds even if notification is lost); node identity = attribution/access only, never a lease. N-1: lease field additive, absent lease degrades to connection-identity fencing.
- **spt-core mapping:** `spt-daemon/src/broker.rs` `OutputLog::resolve_subscribe` (same-identity branch) + `become_controller` slot + `dispatch_input` fence; `spt-daemon/src/brain.rs` documented `None==None` note; `spt/src/rc.rs` loopback attach origin + `PumpEnd::Displaced` (client side already correct).
- **Source:** RC-RENDER-TRUTH W2 (hertz RCA filed 2026-07-18, doyle seam-verified same day; ADR-0044).

### 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]`
<!-- [doc->REQ-HAZARD-TEARDOWN-DEADEND] -->
<!-- [doc->REQ-ENDPOINT-TEARDOWN-AUTHORITY] -->
- **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 / unregistered the address, and stamped the record. Three consequences, all observed: (a) the whole subtree was orphaned, and because the survivor was a GRANDCHILD of the PTY child, even a direct-child kill would have missed it; (b) the stamp was not durable — post-stop `info.json` read `status=offline` beside `rest_state=active` + `controlled=true`, because the surviving host RE-BOUND over the CAS-less `terminal_normalize(path, None)`; (c) it composed with `endpoint run`'s CORRECT `ENDPOINT_CREATE_CONFLICT` refusal (the deliberate no-silent-reattach rule) into a lifecycle DEAD END — the survivor was simultaneously what `stop` claimed to have killed and what `run` refused to work around, so with a wedged harness every in-band verb was exhausted (stop lies, run refuses, `rc` replays a dead PTY, `rc --take` controls a process that never answers) and exit required out-of-band `taskkill`. NEITHER behavior was individually wrong; the COMPOSITION was the hazard.
- **Invariant:** a teardown verb never stamps a terminal or resting state it has not caused. Where the session is broker-hosted (`controllable == Some(true)` — the ONE hosting authority, ADR-0041), "stopped" and "suspended" both mean the broker session row is GONE and its whole DESCENDANT subtree is reaped, via one shared primitive behind both verbs: resolve → dedicated sid/endpoint-keyed kill claiming NO controller (never `attach` + `kill_session`, which is a controller theft with a lease-generation bump behind it) → descendant tree-kill → bounded await of row removal → only THEN stamp + unregister + advertise. The kill never depends on harness cooperation — a wedged host is the design case. On an unconfirmed reap: exit non-zero, stamp NOTHING (a cold row over a live process is the worse lie), and name the surviving root pid plus the scoped-kill remedy, because `stop` is the last rung with no in-band escalation behind it. Where core holds no process (harness-hosted / external) a teardown verb claims nothing about processes. Regression must assert the COMPOSITION: after a `stop`, `endpoint start <same>` SUCCEEDS and never answers `ENDPOINT_CREATE_CONFLICT`. **The refusal is gated on a LIVE process, never on row presence alone** — "the row is still there" is only a proxy for "a host survived". A lingering row whose root pid is provably gone is the ZOMBIE row (`zombie_verdict`'s primary class: a dead root with a surviving record), it can never clear itself because the exit waiter already failed to remove it, and refusing it would strand the record in band while naming a remedy against a pid that no longer exists — the same dead end on a different class of row. **A stamp requires POSITIVE proof the root is gone; no snapshot is not proof** — absence of evidence read as proof of death is the same error class one layer down, so an unavailable or empty process oracle must never authorize a stamp (the illustration: `process_table` yields empty where the platform has no snapshot). Prove that liveness from the OS process table, never from `is_process_alive` — on Windows the latter probes `OpenProcess`, which keeps succeeding for a terminated process while a parent holds its handle, and the broker always holds one for a PTY child (filed separately as a backlog audit). A stale row is stamped LOUDLY, never silently as a clean reap: two stamps that reached the same end state by different truths are not the same event, and collapsing them makes this bug undiagnosable in the field next time. **And a named survivor propagates:** `purge --force` refuses rather than wiping records out from under a live subtree, because the records are the only in-band handle left to it; the ordinary slow-to-quiesce case keeps its clean-anyway posture, since that is not a named survivor.
- **spt-core mapping:** `spt/src/teardown.rs` (the shared primitive + topology gate + timeout copy); `spt/src/cli.rs` `cmd_shutdown` / `cmd_stop`; `spt-daemon/src/broker.rs` `dispatch_teardown` (over `spt_store::proc::kill_pid_tree`); `spt-daemon/src/brain.rs` `teardown_session`; `spt-daemon/src/livehost.rs` `reconcile_hosted_liveness` stays the partial-failure catch-up net. Precedent one scope up: `REQ-HAZARD-DAEMON-STOP-REAP`.
- **Source:** TEARDOWN-AUTHORITY W1 (ADR-0045; hertz RCAs 2026-07-19).

### 7.50 The liveness oracle answers from the process table, never from a handle a caller still holds  `[REQ-LIVENESS-ORACLE-SOUND]`

<!-- [doc->REQ-LIVENESS-ORACLE-SOUND] -->
- **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<PtySession>`, 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 class (`Some(false)` — "dead root, surviving record — always a zombie") onto the conditional arm, where classification then additionally requires `adapter_labeled && past_grace && !has_live_descendants`. **Consequence: a dead-root session that is not adapter-labeled is claimed LIVE indefinitely** — `endpoint run`'s dup-guard refuses `ENDPOINT_ALREADY_LIVE` over a tree that is already dead, and `cmd_rest`'s Suspend `alive_hint` forces `from=alive` on the same false claim (both via `has_live_session_honest`). `REQ-ENDPOINT-CYCLE-HONEST` exists to give the cycle verbs ONE liveness authority; after W1 there were TWO, disagreeing by construction.
- **Invariant:** **`is_process_alive` is unsound exactly when the ASKER — or a live ancestor of it — still holds an open HANDLE to the target.** That is the only condition under which `OpenProcess` outlives termination; dropping a `Child` closes the handle, so a spawner that DROPS is honest and a spawner that RETAINS is not. Therefore 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 very same pid** — soundness is a property of the ASKER, not of the call. "Does this pid still exist" is answered from the OS process table, which lists only processes that still exist, so absence from a NON-EMPTY snapshot is proof of death. An EMPTY table is **NO KNOWLEDGE, never proof** (`process_table()` yields empty where the platform has no snapshot): it must resolve to "unproven" — at the zombie site, to `None`, which already means "never guess" — and must NEVER manufacture `Some(false)`, which would mass-classify every live session a zombie. `zombie_verdict` stays PURE and unchanged: it was fed a lie, it is not wrong. Audit call sites with the discriminator rather than mass-migrating; the sound probe is a NAMED SIBLING whose name is the question (`process_exists`), and `is_process_alive`'s doc names the question it does NOT answer.
- **Second instance — the table's OTHER half (paid-for, found by doyle at the RESIDENT-SERVICE W1 PR gate, Linux leg, 2026-07-26):** absence from a non-empty snapshot is proof of death, but **PRESENCE in it is proof of LIFE on Windows only.** On unix a killed-but-unreaped child of a still-running spawner (in-process daemon hosting, tests) stays in `/proc` in `Z` state with a readable `stat`, so `process_table` keeps it and `process_exists` answers `Some(true)` for a process that runs nothing. `provably_gone`'s present arm returned `!exists` unconditionally, so table-presence beat the `Z`-state demotion `is_process_alive` already performs: a completed force-kill polled `awaits_death` to its full deadline and reported `OrphanSweep::KillFailed`, and a tree-teardown assertion read its own direct child as a survivor. Windows was structurally green (Toolhelp snapshots exclude terminated processes; no zombie concept) — a Windows-only gate would have shipped it. **Invariant:** the present arm asks `is_process_alive` on unix (`kill(pid, 0)` + the `/proc` `Z` read — both handle-independent, so this does NOT reopen the `OpenProcess` hazard above, which is Windows-only) and answers "alive" from the table alone on Windows. A zombie is GONE to every consumer of "is a process I own gone?".
- **spt-core mapping:** `spt-store/src/proc.rs` (`is_process_alive` caveat + `process_exists` sibling over `process_table` + `provably_gone`'s per-platform present arm); `spt-daemon/src/servicehost.rs` `awaits_death` / `kill_orphan_service_at`; `spt-daemon/src/broker.rs` `session_is_zombie` (`wrapper_alive`) with `zombie_verdict` unchanged; consumers `spt/src/rc.rs` `has_live_session_honest`, `spt/src/cli.rs` `endpoint run` dup-guard + `cmd_rest` Suspend `alive_hint`; `spt/src/teardown.rs` `root_provably_gone` (handle-half correct since W1, zombie-half fixed 2026-07-26 — the mapping vouches only for what was audited). Precedent one scope down: `REQ-HAZARD-TEARDOWN-DEADEND` (row-presence-as-proxy).
- **Source:** TEARDOWN-AUTHORITY W2 (ADR-0045 Amendment 1; todlando W1 gate finding 2026-07-19). Knowledge note: this Windows behavior was documented VERBATIM in `spt-daemon/tests/legacy_resident_sweep_e2e.rs` (which drops handles deliberately to get an honest probe) and never reached `proc.rs` or `zombie_verdict` — written where DISCOVERED, not where CONSUMED.

### 7.51 Process custody is an identity, never a bare PID — a recycled pid must read NOT OURS  `[REQ-HAZARD-RESUME-CUSTODY-ABA]`

<!-- [doc->REQ-HAZARD-RESUME-CUSTODY-ABA] -->
- **Failure (paid-for, hertz v0.39.4 field RCA 2026-07-22, doyle code-verified same day):** `resume.pid` custody is a bare PID consumed as `read_resume_pid(..).is_some_and(is_process_alive)` at BOTH the livehost restart gate and the liveness-reconcile DEFER. A dead wake-resume spawn's pid, recycled by the OS onto an unrelated process (field proof: `resume.pid=29456` resolved to a random `cmd.exe`), reads as "a resume is in flight" indefinitely: reconcile defers every tick, the row stays online-authoritative, and the endpoint is FALSE-ONLINE with no self-repair — every reader faithfully resurrects the lie. The ABA problem, in process custody.
- **Invariant:** a custody record that gates lifecycle decisions stores an **identity pair (pid + process creation time)**, written atomically at spawn-mint; consumers test the PAIR, and a mismatch means NOT OURS — the discovering reader DELETES the record and proceeds as if no custody existed (self-heal, not error). Bind and reap clear custody atomically with their own outcome. Creation time comes from the process snapshot, never a retained handle (7.50); no snapshot ⇒ "unproven" ⇒ defer ONE tick, never a manufactured verdict.
- **spt-core mapping:** `spt-daemon/src/livehost.rs` (restart gate + reconcile DEFER custody reads); `spt-store` resume-custody record + `proc.rs` snapshot identity; regression = the recycled-pid rig (custody pair mismatching a live impostor pid → record deleted, reconcile proceeds, row goes honest).
<!-- [doc->REQ-BROKER-ZOMBIE-IDENTITY] -->
- **Zombie-reap kill boundary (releases#285):** `broker.rs` captures the PTY root's creation time before starting its exit waiter; a later zombie verdict alone never licenses killing that remembered number. Fresh death or a different live birth proves the remembered child gone and permits cleanup without a signal; only a matching live birth permits tree-kill. `SPAWN_ZOMBIE_REAP_RECYCLED` identifies both births before de-tabling and clearing stale stamps so respawn can proceed. Unknown identity still emits `SPAWN_ZOMBIE_REAP_DECLINE`: neither tree-kill nor forced de-table runs, including any resource-drop fallback. The regression changes the recorded birth of a real PTY child, exercises the spawn gate's forced cleanup and stamp clearing, and verifies a subsequent spawn succeeds while the other identity survives. This is not atomic with the numeric tree-kill; the check-to-signal race and Linux same-tick identity collision remain (ADR-0041 decision 6).
- **Source:** DAEMON-LIFECYCLE W1 (ADR-0047 decision 1; hertz field RCA bug 1).

### 7.52 An operator stop outranks every implicit ensure — no convenience path resurrects what the operator just killed  `[REQ-HAZARD-STOP-RESPAWN-CONVOY]`

<!-- [doc->REQ-HAZARD-STOP-RESPAWN-CONVOY] -->
- **Failure (paid-for, hertz v0.39.4 field RCA 2026-07-22, doyle code-verified same day):** `spt daemon stop --force` is non-terminal on a box hosting live adapter sessions: every `spt api` invocation runs an unconditional `ensure_daemon()` (REQ-DAEMON-3's anchor), hook-driven api calls arrive continuously, so the stop is a race the operator loses — 5–10 ephemeral spawner windows flash and the daemon is back; several force-stops to stay down. The rc-side twins were fixed earlier (rc.rs); the api anchor stayed armed. Two defects compose: resurrect-after-stop, and N racing spawners with no serialization.
- **Invariant:** `daemon stop` records a durable machine-scoped **stop inhibit** BEFORE teardown begins; every implicit autostart path consults it and DECLINES with one honest line naming the remedy (`spt daemon start`). Cleared by intent verbs only (explicit start; update paths that restart by design) — **no TTL** (a timeout is the surprise respawn again, later). Implicit autostart additionally takes a machine-wide lock around probe-and-spawn so concurrent callers can never launch competing daemons. Regression must assert the COMPOSITION: stop under a live api-call storm → daemon stays down, zero respawns, callers print the refusal; explicit start clears; concurrent-ensure race spawns exactly one daemon post-clear.
- **spt-core mapping:** `spt/src/api/mod.rs` `ensure_daemon` + `spt-daemon` `ensure_running` (the two implicit anchors); `spt/src/cli.rs` `cmd_stop` (inhibit mint) + `daemon start` (clear); machine-lock alongside the existing daemon single-instance machinery. Kin: `REQ-HAZARD-DAEMON-STOP-REAP` (stop reaps its tree — this hazard keeps the tree DOWN afterwards).
- **Source:** DAEMON-LIFECYCLE W1 (ADR-0047 decision 2; hertz field RCA bug 2).

### 7.53 A durable copy of revocable content is validated against its source of truth at DELIVERY time — a spooled notice outliving its row must not deliver  `[REQ-NOTIF-DRAIN-ROW-VALIDITY]`

<!-- [doc->REQ-NOTIF-DRAIN-ROW-VALIDITY] -->
- **Failure (paid-for, operator field report from perri's node 2026-07-22 — the day after the v0.40.0 notif redesign shipped; doyle root-caused same day, own session corroborating with stale v0.39.x drains):** a node already ON v0.40.0 received the v0.40.0 "update available" notice twice. ADR-0046's quiet delivery makes every notif surface an `active_only` SPOOL write per endpoint; every lifecycle mechanism (apply-seam dismissal, coalesce supersession, TTL, migration) operates on notif-store ROWS only. A copy spooled while the endpoint was busy/offline — the normal state of a working agent — is a detached snapshot no dismissal can recall; it delivers at the endpoint's next drain, after the fact it advertises has been resolved, once per qualifying surface event. The row-layer claim "notices self-clear" was true; the delivery layer the operator sees lied.
- **Invariant:** content that is REVOCABLE at its source (a dismissible notif row) is re-validated against that source at the moment of DELIVERY, not only at the moment of surfacing. The safe-point drain delivers a notify envelope only if its `notif_id` resolves to a live undismissed row; dismissed/superseded/expired/unknown copies drop silently, and N spooled copies of one notif dedupe to one delivery. Ordinary (non-revocable) messages are exempt and MUST deliver. Recall-on-dismiss sweeps are the rejected shape (race the drain, span every perch, miss copies in flight) — validity lives at the choke point every copy must pass, not at N producers.
- **spt-core mapping:** `spt/src/api/delivery.rs` `cmd_poll` deferred presentation (the one drain choke point); notify envelope attrs (`from`/`notif_id`/`subnet`, notif.rs `notify_envelope`); `spt-store/src/notif.rs` row-liveness read. Kin: 7.49/7.50 (a stamp/verdict needs positive proof at the moment it is asserted — same principle, delivery-shaped).
- **Source:** DAEMON-LIFECYCLE W1 rider (ADR-0046 Amendment 1; operator field report 2026-07-22).

### 7.54 A geometry change invalidates the client's painted cells by itself — every resize entry emits the authoritative repaint, and an attach's old-geometry repaint is never the final word  `[REQ-ATTACH-RESIZE-REPAINT]`

<!-- [doc->REQ-ATTACH-RESIZE-REPAINT] -->
- **Failure (paid-for THREE TIMES — v0.39.3 geometry-epoch, v0.39.4 presentation barrier, and still red in the field; pinned by hertz's ENLYZEAM production byte capture 2026-07-22, doyle seam-verified same day):** the codebase has TWO resize entries. The IPC `ResizeReq` path is sound post-0.39.4 (arm barrier → settle → issue → commit pushes the authoritative repaint at the landed geometry). But rc's INITIAL viewport resize rides the attach-stream verb (`send_attach_resize`, once per establish) which never arms the barrier — the no-transition branch of `commit_resize` resizes the grid and pushes NOTHING, on the claim "no suppression, so no sync frame owed". So a controller attach to a session hosted at different geometry (80x24 hosted, 131x60 client — no window resize anywhere, which is why the operator's "not resize-related" was true as observed and the seam hid for three iterations) paints the OLD-geometry synthesized repaint, lands the new geometry silently, and every subsequent differential assumes the reflowed model against a client screen still holding pre-resize cells — keystroke-paint `/c` scraps and SGR misalignment. Proof both directions: independent-emulator replay WITH the stale repaint reproduces; from the first post-resize full frame is clean; fresh viewer clean; transport byte-identical (A/B taps).
- **Invariant:** a geometry change invalidates every attached sink's painted cells BY ITSELF — bytes-in-flight or none. Geometry change is ONE transaction with ONE exit shape regardless of entry point: resize → grid lands → authoritative full repaint at the new geometry to every attached sink → only then subsequent output. Any resize entry (IPC, attach-stream, future) rides the SAME barrier machinery; a "no repaint owed" branch survives only for genuinely sink-less sessions. At attach specifically: when viewport geometry differs from hosted geometry, the initial resize's committed repaint supersedes the attach's synthesized old-geometry repaint — the old-geometry paint is never the client's final word. ORACLE: regression replays the captured shape (content-bearing 80x24 hosted → 131x60 controller attach → menu/keystroke differentials) into an INDEPENDENT VT emulator and asserts final cells/colors — a ScreenGrid-only oracle shares the model under test and misses this class by construction (the REQ-SCREENGRID-WIDTH precedent).
- **spt-core mapping:** `spt-daemon/src/broker.rs` `commit_resize` no-transition branch + the attach-stream resize handler (`send_attach_resize` server side) vs the barriered `ResizeReq` dispatch; `spt/src/rc.rs` initial `send_attach_resize` at establish; kin REQ-RC-RESIZE-GEOMETRY-EPOCH / REQ-RC-RESIZE-PRESENTATION-BARRIER (the machinery exists — this hazard is the second entry that must ride it).
- **Source:** DAEMON-LIFECYCLE W3 (ADR-0047 Amendment 1; hertz ENLYZEAM byte-capture RCA 2026-07-22, v0.39.4 field bug 3).

### 7.55 A surface resize never alters the hosted terminal's input discipline — input bytes must never come back as output  `[REQ-RESIZE-INPUT-MODE-INTEGRITY]`

<!-- [doc->REQ-RESIZE-INPUT-MODE-INTEGRITY] -->
- **Failure (paid-for, second ENLYZEAM production capture 2026-07-22 — operator resized a LIVE established rc viewport; hertz froze the taps):** after the resize, the raw ConPTY drain emits the operator's keystrokes as isolated OUTPUT records — literal one-byte `c`/`o`/`n`/`f`/`i` frames interleaved with the TUI's cursor-addressed menu diffs, raw and broker taps byte-identical. The hosted Claude TUI runs raw/no-echo, so the hosted console echoed input that nothing asked it to echo. That evidence is REAL and is SERVER-SIDE: it sits in the child→broker drain, upstream of any client console, so it is independent of the 7.56 presentation defect that explains the `/c/config` line's other half. Non-spt sessions bypass the nested ConPTY path, which is why the operator only ever saw it spt-hosted. spt-core touches console input modes NOWHERE in-tree (zero `ENABLE_*` handling).
- **MEASURED 2026-07-22 (todlando, doyle-ratified), and it changes the shape of this hazard:** the resize seam does NOT alter the input mode. A probe child under a real ConPTY, clamped raw at startup, resized 24x80→60x131 live, reports an IDENTICAL input mode word at four sample points — `boot` / `before-resize` / `after-resize` / `before-write`, all `in=0x000001f0 echo=0 line=0` — and typed bytes do not come back as output. A seeded capability probe (cooked input deliberately re-enabled mid-run) moves BOTH observables, so the absence is earned rather than vacuous. ADR-0047 Amendment 1's addendum supposed "something across the resize path re-enables console echo"; on this box and this `portable_pty`/ConPTY version, that is NOT what happens. Trigger UNPINNED.
- **The open candidate (field question with hertz, NOT built against):** the pseudoconsole BOOTS with `ENABLE_ECHO_INPUT`/`ENABLE_LINE_INPUT` ON (`in=0x000001f7` measured) and it is the CHILD that clamps them off. Any window in which the hosted TUI has not yet clamped — startup, or a re-clamp after a TUI-internal state transition — is a window in which the console echoes BY DEFAULT, with no seam having "re-enabled" anything. That reframes the question from *who turns echo back on* to *is there a window where nobody has turned it off yet*. It is a candidate, not a root — but it is now FIELD-GROUNDED (hertz via doyle, same day): in capture 2 the echo onset is **6003 ms after** the first resize-associated repaint burst and lands immediately after a 3225-byte TUI-reinit-shaped absolute repaint, then ceases mid-input (`c`/`o`/`n`/`f`/`i`, no `g`) right after a 535-byte diff — a late clamp landing, invisible to a byte tap, and NOT resize-instant. Carried forward as seed `REQ-TERM-ECHO-CLAMP-WINDOW` (minted inactive); note that spt may not be able to mitigate it at all, since the daemon cannot touch the child console's modes, so the honest outcome may be an upstream finding.
- **Invariant:** input bytes must never come back as output — that stands, and it is the observable any regression asserts, because it survives being wrong about the mechanism. Two structural facts constrain every fix and every rig for this class: (1) **the daemon cannot observe or set the hosted child's console modes at all** — those handles belong to the pseudoconsole the child is attached to, and the daemon holds only the master end, so "instrument the mode at the `spt-term` resize seam" names a probe point that physically cannot see the console in question, and a server-side clamp is equally impossible; the only vantage point is a process attached to that console; (2) consequently the RIG is the instrument, and no production instrumentation ships for this hazard (measuring the wrong console is manufactured confidence) — if the field ever needs live diagnosis, the probe-child pattern ships as a debug tool then.
- **spt-core mapping:** no production code owed today. Instrument = `adapters/mock/src/console_mode_probe.rs` (probe child; verdict travels in the stdout PROTOCOL LINE, never stderr — under a ConPTY the child's stderr is interleaved into the same re-rendered stream and arrives shredded, so a refused `SetConsoleMode` could otherwise read as a measurement). Rig = `crates/spt-term/tests/resize_console_mode_integrity.rs` (the four-sample measurement, the typed-bytes-do-not-echo symptom leg, and the seeded capability probe). Rig-craft pinned there and transferable: a probe that never clamps raw is already sitting at the default the seam is suspected of restoring and cannot distinguish "reset" from "never changed"; and ConPTY's post-resize REPAINT re-emits earlier output, which impersonates a fresh reply to any rig that matches loosely.
- **Source:** DAEMON-LIFECYCLE W3 (ADR-0047 Amendment 1 addendum; hertz second production capture 2026-07-22; measurement + rescope todlando 2026-07-22, doyle-ratified same day).

### 7.56 The rc client presents relayed bytes exactly as the emitter addressed them — console output modes are part of presentation truth  `[REQ-RC-NEWLINE-PRESENTATION-TRUTH]`

<!-- [doc->REQ-RC-NEWLINE-PRESENTATION-TRUTH] -->
- **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 never asked for; later absolutely-addressed writes never erase cols 1-2, so the `/c` residue survives to the final screen. The relay is BYTE-CLEAN end to end — which is exactly why two independent isolated rigs read NOT-REPRODUCED (they compared bytes or rendered via spec VTs; nobody presented through a really-configured Windows console), why a fresh viewer read clean (the synthesized attach repaint is CUP-absolute, no bare LFs), and why the debris appears at a FIXED window size (no resize required). The W3 replay rig's "vehicle artifact" (a second ConPTY manufacturing the exact field string) was this same asymmetry — the rig was a faithful model of the CLIENT while being consulted about the broker.
- **Invariant:** the rc presentation path configures the console so relayed bytes render as the emitter addressed them: whenever processed/VT output is enabled, `DISABLE_NEWLINE_AUTO_RETURN` is set with it (the Windows half of the symmetry Unix rc already has — raw mode clears OPOST/ONLCR); the prior mode is captured and restored on exit as today. A bare LF in the relayed stream is an INDEX (row down, column preserved), never a newline. Console-mode seam kin: 7.55 (input echo across resize) and the W3 vehicle finding — three manifestations of one uninstrumented seam in one day; any rig placing a non-spt vehicle (ConPTY or otherwise) between fixture and code-under-test must prove the vehicle transparent per cell before its verdict counts (the vehicle-fidelity clause), or declare itself a client-model rig in its header.
- **spt-core mapping:** `spt/src/rc.rs` `with_vt_output`/`enable_vt_output`/`restore_out_mode`; regression = the W3 captured-bytes rig PAIR (vehicle console without DNAR = field client model, RED pre-fix; with DNAR = fixed client, GREEN) + the minimal LF probe (`ESC[2J ESC[5;3H "XX" LF "AB"` — column preserved vs reset) through an rc-mode console.
- **Source:** DAEMON-LIFECYCLE W3 bisect (todlando three-probe isolation + doyle static grep + byte-verify, 2026-07-22); hertz ENLYZEAM captures (anchor + fresh-viewer-clean contrast).

### 7.57 Resolve a process-global once per operation and thread the value inward  `[REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION]`

<!-- [doc->REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION] -->
- **Latent failure:** a public operation resolves a process-global and then calls a callee that resolves it again. The two reads can disagree inside one operation. Two production instances existed unnoticed: `daemon_inhibit::set_stop_inhibit -> inhibit_path` and `servicehost::service_env -> env assembly`. Both were invisible until a test happened to be the victim; nobody was hunting the production forms.
- **Invariant:** resolve the process-global exactly once at each public boundary and thread that value inward. An `_at(home)` helper is not proof if any inner callee can still read the global.
- **Eventual gate:** count `spt_home()` occurrences and require exactly one per public boundary, inside the thin wrapper. The repaired files currently expose the intended count directly: `servicehost.rs` has one occurrence in its wrapper; `daemon_inhibit.rs` has five, one per public function. A shape-only test for a threaded parameter is insufficient because it can pass while a callee re-reads the global.
- **Posture:** recorded as inactive `REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION`; activate only with the milestone that adds the counting regression. No changelog entry: disagreement requires an in-process `SPT_HOME` mutation and is not user-observable in ordinary CLI use.

### 7.58 A recorded pid is not kill authority — authenticate the process before any kill, in rigs AND in product  `[REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY]` `[REQ-SHELL-KILL-AUTHENTICATED]`

> **Scope note:** the requirement id keeps its `-TEST-` infix for registry stability (it is referenced by markers and groups across the tree), but this entry is **not** test-scoped. It was written from a rig failure and read as a rig-discipline rule until BAROMETER proved the same defect shipped in the product kill path; the product half is tracked by `REQ-SHELL-KILL-AUTHENTICATED`.

<!-- [doc->REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY] -->
- **Failure (golden run `30501468421`, Windows job `90742055246`):** `worker_lifecycle_e2e` ran all child calls, then read `daemon.pid` and invoked `taskkill /PID <pid> /F /T` before its first assertion or observability point. A hard-killed harness cannot unwind, producing the observed bare exit 1 with no panic or assertion. More seriously, a stale or wrong recycled pid lets `/T` terminate an arbitrary foreign process tree on a shared runner.
- **Invariant (BOTH populations — test rigs and shipped product):** identity precedes termination. First distinguish a gone process from a live one: an already-exited pid is benign, logged, and skipped. For a live pid, prove the process wearing that number is the one the record means before any kill — in a **test rig** by re-resolving its executable path and requiring it under that job's own target directory (refusing the current test process and every ancestor even when the path matches); in **product code** by the pid+birth PAIR (`liveness::relay_liveness`), since a birth stamp is what the daemon can park at launch and an image path is not sufficient there — two daemon-launched shells share an image. **A live but unidentifiable process is never a legitimate kill target in either population:** unreadable or denied identity fails closed, leaves the process alive, and says so — loudly failing the test on the rig side, a named refusal line on the product side. A helper that collapses “gone” and “unreadable” is not adequate for a kill path.
- **The refusal must be audible.** A destructive path that treats unknown as do-not-fire trades a loud wrong action for a quiet wrong state, and the quiet one is what cannot be RCA'd later. Every declined kill names its reason and the pid it spared, and any bounded confirm loop that ends with the target still alive reports that rather than returning as though it had succeeded.
- **Measured breadth, predicates stated separately:** on the wider non-recursive root `crates/*/tests/*.rs`, 42 files contain `taskkill` plus the literal argument `"/T"` (the original `crates/spt/tests/*.rs` root accounts for 31). Of those 42, 30 also join a literal `*.pid` or `*.ready` breadcrumb filename: 26 reference `brain.ready`, 6 reference `daemon.pid`, with overlap; manual source tracing confirms breadcrumb-to-tree-kill flow in 23 and leaves 7 explicitly unconfirmed. An independent wider-root predicate reproduced exactly the original 6 `daemon.pid` tree-kill flows. A graded second tier adds 12 more files that manually pass `daemon.pid` to `taskkill /F` without `/T`: smaller blast radius, identical reused-pid identity violation. Excluded populations remain source unit modules, nested `tests/common/`, scripts, and any non-literal or differently named breadcrumb until separately enumerated.
- **`brain.ready` inversion:** a live brain refreshes `brain.ready` every heartbeat, so **a stale `brain.ready` means the brain is already dead — and killing a dead pid is precisely when reuse bites.** A heartbeat-refreshed breadcrumb feels safer than a write-once one, but staleness correlates with the original target being gone; the dangerous reuse case is therefore the common stale-file case.
<!-- [doc->REQ-SHELL-KILL-AUTHENTICATED] -->
- **PRODUCT POPULATION (BAROMETER, releases#114 — this entry was scoped TEST-only until then, and that scoping was false the moment the product defect was proven).** The shipped shell lifecycle carried the identical violation. BAROMETER W2 authenticated the DERIVATION path (`shellinfo::shell_pid_provably_dead` runs the pair test) and left the KILL path believing a bare pid, so the very state the derived read calls a corpse still routed to `taskkill /PID <pid> /T /F`. Proven by probe, not inferred: a live stranger's pid parked beside a foreign birth stamp is killed by `kill_shell_at` while `shell_pid_provably_dead` calls that same pair a corpse. **Site shape is two chokepoints, not three call sites** — `shellhost::kill_shell_at` (reached by `close_shell` and the `spt shell teardown` force path) and `shellwake::kill_waker_at` (reached by `launch_shell`, `close_shell`, `stop_watcher`, the reconciler), both funnelling into the single `kill_shell_pid`; six product callers, one taskkill, so the gate lands in the two chokepoints and no caller changes. Repaired under `REQ-SHELL-KILL-AUTHENTICATED`.
- **A read-side gate is worthless where nothing was written to read.** `waker.pid` was parked bare, and `relay_liveness(live_pid, None)` is `Held` by design (pre-stamp records fail toward alive) — so a gate at `kill_waker_at` alone would have returned `Held` for every record that exists today and every one written tomorrow: a **vacuous** gate that proves green. Authenticating a kill therefore begins at the WRITE site. The pair is minted in the waker's single existing write as a two-line `waker.pid` (`<pid>\n<birth>`): one file, one write, atomic — a stamp in a second file leaves a window where the pid is on disk and the stamp is not, and that window reads `Held`, which is the unauthenticated kill reintroduced as a race. Write and every reader go through one shared `record_waker_launch`/`read_waker_launch` pair.
- **The pair test NARROWS the mis-fire; on Linux it does NOT eliminate it.** `process_started_at` reads `/proc/<pid>/stat` field 22, which counts `CLK_TCK = 100` jiffies, so two processes whose starts fall in the same 10ms tick carry an identical stamp and the pair cannot separate them. The window closes from *any recycled pid* to *a pid recycled into the same 10ms tick as the recorded start* — which requires our process to die AND the OS to hand its number to a new process inside one tick. Windows is unaffected (`FILETIME`, far finer resolution). Measured by hertz on kitsubito; recorded here so nobody re-derives it, and so the text promises a narrowing rather than a cure. **The residual window's precondition is its own mitigation** — no extra mechanism is owed (the image-path corroboration that would unify this rule's two populations on one predicate is filed as releases#120, deliberately not taken in-milestone). **And note the asymmetry that makes this the kill path's problem to state:** the same predicate with the same imprecision is safe on the DERIVATION side and unsafe here — `Held` means "stays stale" for a derived read and "fire the kill" for a destructive one, so only the kill side carries the residual harm and only the kill side has to say so.
- **Migration window and downgrade direction, named rather than discovered.** Pre-W2 shell records and pre-fix waker records carry no stamp, read `Held` on existence alone, and keep the old bare-pid kill until their next launch writes a pair — the window is accepted, not closed. In the other direction, an OLD binary reading a NEW two-line `waker.pid` parse-fails to `None`, which reads as no-pid and refuses the kill: missed-kill, never mis-fire.
- **Repair posture:** the release-path repair instrumented and path-authenticated only `worker_lifecycle_e2e` on the rig side; that narrowness still holds for the 42-file rig breadth, which remains a finding rather than permission for silent scope expansion. It never licensed leaving the product path unauthenticated, and no longer reads as though it did.

#### 7.58b A best-effort breadcrumb is not an identity oracle on the READ side either

<!-- [doc->REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY] -->
The kill-path rule above generalizes: `daemon.pid` is not authority for *"which process is the daemon"* in any direction, not merely *"which process may I kill"*. Reading it to decide **whose** answer counts is the same violation with a quieter failure — no foreign process dies, a feature just silently never runs.

- **Why the file lies even when nothing is stale.** `daemon.rs` writes the breadcrumb **before** `serve_seed_control` binds the seed socket, and that bind is what arbitrates single-daemon-per-home. A second starter therefore reaches the write, overwrites the winner's pid, *then* loses the bind and exits. The file now names a dead pid while the real daemon serves. Measured on HFENDULEAM 2026-07-31: `daemon.pid` = 40332, dead since startup; the daemon actually serving = 23416, started one second earlier. Both processes behaved exactly as designed.
- **How it bit (releases#43).** The inbound-reachability verify was first gated on `daemon.pid == std::process::id()` — "only the resident daemon verifies". On the box it was written on, that predicate is false inside the real daemon, so the feature would have been disabled in production while every test passed. A gate that reads the fault as "not me" fails silent, which is why this belongs with the 7.61 family: nothing renders, and nothing renders when all is well either.
- **Invariant:** identity is re-derived, never asserted by a file. Validate a recorded pid against the **process table plus the image path** — the pid must still resolve to an image and that image must be the recorded one (a pid alone is recyclable). And where the question is *"am I the daemon"*, do not ask an oracle at all: put the call at a site that **is** the daemon by construction — after its own successful bind — so no file is consulted.
- **Corrected pattern:** `spt_store::inbound` (record carries binder pid + image path, reader re-derives validity) with its trigger in `spt_daemon::daemon::try_start_net`'s success arm rather than inside `NetHost::start`, which the CLI also calls.
- **Open audit surface, stated not fixed:** the other `daemon.pid` readers — `crates/spt/src/cli.rs` ~4908 and ~5043 — were not audited by that change. Each needs its own reading of what it uses the value FOR; a display of "the daemon's pid" is not the same claim as a decision gated on it. Tracked separately on the board (doyle), not silently absorbed here.

---

### 7.59 A peer is never stranded by an address that has ALREADY failed — 7.42's second clause  `[REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP]`

<!-- [doc->REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP] -->
- **Failure (paid-for, hertz field RCA 2026-07-30/31 — HFENDULEAM cross-site invisibility, live-fire proof on the running daemon):** 7.42's fix retained the roster address and taught the resolver to prefer it. The unstated assumption behind that preference — that a retained roster row is fresher than the address that just failed — was false fleet-wide, because every node's self row was frozen by a regressed lease (7.60). The result is a livelock, not a strand: `resolve_submit_addr` leg 2 re-served the same dead roster address on every round (leg 3, id-only discovery, was unreachable while any roster address existed), and `reconcile_peeraddrs`' suspect arm reinstalled that byte-identical address over the demoted cache row and cleared the suspect mark on every roster exchange. Injection timeline on the live daemon: fresh addresses written 19:40:47, dialed 19:42:09, failed 19:42:19 (demote), clobbered back to the stale roster values with the mark cleared by the 19:42:34 exchange. `fail → demote → reinstall-same → fail`, forever, with `net_up: true` and a fresh heartbeat throughout.
- **Invariant:** the node keeps a node-LOCAL, RAM-only, TTL-bounded memory of the exact ADDRESSES its dials failed on — keyed on the full address actually dialed, by exact equality, built at the dial site and cleared by a successful dial or validation of that address. No path may re-serve or reinstall a just-failed address: route-chain leg 2 skips a roster address identical to it and falls through to discovery, and reconcile refuses to reinstall it over a suspect row (leaving the mark standing). A **differing** roster address still resolves and still replaces a suspect row with the mark cleared — 7.42's first clause is intact and this is an added clause, never a cross-site carve-out. The memory never rides the gossip/roster DTO (a local fact stays local) and needs no persisted store: a restart forfeits it and costs one wasted dial.
- **spt-core mapping:** `spt-daemon/src/failedaddr.rs` (the shared record + the ONE equality site); `spt-daemon/src/nethost.rs` `submit_dial` (built here — the site that owns the dialed address — and carried to the pump on `PRESENCE_DIAL_FAILED.remote_addr`); `spt-daemon/src/pump/mod.rs` `resolve_submit_addr` leg 2 + `presence_state_effect`; `spt-daemon/src/seedproofx.rs` `reconcile_peeraddrs` suspect arm.
- **Source:** WAN-TRUTH batch #50 lane 1 (issue #40; hertz RCA + live-fire injection 2026-07-30; doyle's address-keyed ruling 2026-07-31, which amended the RCA's per-leg/per-peer F2/F3 wording precisely because that shape would have deleted 7.42's connection-free strand-heal). Kin 7.42 (`REQ-HAZARD-MESH-BOOTSTRAP-TRAP`, the first clause), 7.60 (the lease freeze that falsified the freshness assumption).

---

### 7.60 A node's advertised address is repairable by the node itself — a regressed epoch counter must never freeze its own roster row  `[REQ-HAZARD-SELF-ROW-LEASE-FREEZE]`

<!-- [doc->REQ-HAZARD-SELF-ROW-LEASE-FREEZE] -->
- **Failure (paid-for, hertz forensics 2026-07-30, crash-correlated to the minute on two independent nodes):** `EpochSource::next_epoch` persists through the NON-durable atomic write, whose own doc classifies epoch counters as reconstructible state for which a post-crash NUL husk "simply parse-fails to absent and is regenerated". That classification was the defect: `load_from` silently degrades a husk to `0`, and the counter's VALUE is the fleet-wide lease authority, so `merge_entry`'s strictly-greater rule then rejects every self-advertisement the node makes against its own pre-crash high-lease row — on every peer, forever. Nothing regenerates a counter past its historical maximum. HFENDULEAM: counter 314616 against its own fleet-replicated row at lease 803460 carrying a Jul-18 port, hard resets Jul-19 00:21/02:18. ENLYZEAM: Kernel-Power 41 seventy-two seconds after authoring its last winning row at lease 690512, counter reloaded at 0. At ~19 writes/min the file is rewritten every ~3s, so a hard reset almost certainly lands inside the rename-to-flush window. Every status surface stayed green; the frozen rows carried a fresh `last_seen` riding a dead address, which is what made the fleet believe stale routes were current.
- **Invariant:** before authoring its own roster entry — at boot and on every exchange-provider pass — a node consults the fleet-replicated lease it holds for its own pubkey and fast-forwards its `EpochSource` strictly past that ceiling. The heal is retroactive (an already-damaged node repairs itself on its next boot with zero operator surgery), which is why it, and not counter durability, is the primary fix. Defense in depth rides along: the epoch file is written durably, a NUL husk is distinguished from an absent file and logged loudly rather than degrading in silence, and `atomic.rs` no longer names epoch counters as safe-for-non-durable reconstructible state — that sentence is a documented-false invariant. A node that cannot change its own advertised address is a forbidden state, not a degraded one.
- **spt-core mapping:** `spt-store/src/epoch.rs` `load_from` + `next_epoch` + the fast-forward; `spt-store/src/roster.rs` `upsert_self` (the authorship site) + `merge_entry` (strictly-greater); `spt-daemon/src/seedproofx.rs` `production_roster_exchange` provider (the lease peek); `spt-store/src/atomic.rs` (the reconstructible-state doc list).
- **Source:** WAN-TRUTH batch #50 lane 1 (issue #40; hertz epoch-regression RCA 2026-07-30). Kin 7.59 (the livelock this freeze fed), `REQ-HAZARD-REGISTRY-EPOCH-LEASE` (the counter's monotonicity contract, which this does not weaken — a fast-forward only ever moves it up).

---

### 7.61 A monitor's green must not be producible by the fault it watches  `[REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE]`

<!-- [doc->REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE] -->
- **Failure (paid-for twice in one batch, on the same surface):** a health surface whose green state can be *reached by the very failure it exists to report* does not merely miss the incident — it actively certifies the broken system as healthy, which is worse than having no monitor at all, because it terminates the operator's investigation. **Instance 1 (2026-07-10 sequester):** `net_up` meant "a local NetHost exists", the heartbeat meant "the loop is alive", and the durable counts meant "membership" — every one of them stayed green while every dial failed, because none of them was a function of reaching a peer. **Instance 2 (this entry's own lane, issue #41):** after the reachability verdict was added, `verdict()` returned `Healthy` on `live_peers > 0` *before* consulting the failing set, and the only incident window required EVERY target to be failing. So 2 of 7 peers live with the other 5 failing for days read `healthy`, and any single dial success closed the incident window outright. The second instance is the sharper lesson: the fix for the first was itself written with the same shape, because "at least one thing works" is the intuitive spelling of green and it is exactly the spelling the fault can satisfy.
- **Invariant:** a monitor's green must be a statement about the ABSENCE of the fault, never about the PRESENCE of some success. Concretely: green requires that nothing is failing, not that something is working; a partial fault renders its own verdict rather than collapsing into either neighbour; and an incident window may be closed only by the fault clearing, never by one participant's recovery, never by elapsed time, and never by a heartbeat. Any state reachable *by the fault* that the surface reports as green is a forbidden state. Corollary for eviction paths: dropping the failing subject from the monitored set silently produces green too — a monitor that stops watching what it was failing on has not recovered, and any set-membership change must be examined as a re-entry path.
- **Known live re-entry door (documented, not yet closed):** `PumpHealth::set_targets` retains only current fan-out targets, so a peer that leaves the roster drops out of `failing` and both the partial verdict and its incident clock clear with no recovery having occurred. Harmless while roster membership changes only deliberately; a live re-entry path for this hazard the moment any unreachability-driven staleness sweep evicts peers. Pinned explicitly by a test rather than left implicit (`a_target_leaving_the_roster_drops_its_failure_and_can_return_the_node_to_green`); ownership of any such sweep sits with the roster surface, not here.
- **Instrument-soundness kin (distinct, do not merge):** a *gate* whose failure signature is indistinguishable from the fault it guards is broken in the same family, and instrument tests are never requirement evidence (see the §7 note on the spawn rigs). This entry is about a PRODUCTION monitor's reported state; that one is about a TEST's verdict.
- **spt-core mapping:** `spt-daemon/src/pump/health.rs` — `PumpHealth::verdict` (the failing set outranks the live count; `DegradedPartial` requires a live peer so nothing-live stays `Connecting`), `any_fail_since_ms` + `recompute_all_fail` (the window one peer's success cannot close), `note_connected` (closes windows through the shared recompute, never by hand); `spt/src/cli.rs` `render_peer_health_line` + `peer_health_verdict_token` (the `degraded-partial` surfaces). **releases#75 addendum:** a third verdict `PeersAbsent` now sits AHEAD of both degraded arms (`peers_are_absent`), and it is deliberately gated on a degraded verdict being the alternative — it cannot be reached with an empty failing set, so it opens no new door to green, and it requires positive per-peer registry evidence rather than a live sibling's success, so it does not re-open instance 2 from the other side. The same lane made `live_peers` the intersection of the live connections with the current targets (`recompute_live`), which narrows the sequester test in the honest direction: a live connection to a node we no longer fan out to used to hold `all_fail_since_ms` shut while every actual target was failing.
- **Source:** WAN-TRUTH batch #50 lane 3 (issue #41; doyle-ruled 2026-07-31 after the class appeared twice in one batch). Twin `REQ-PEER-HEALTH-PARTIAL-DEGRADE` (the positive requirement that repairs instance 2). Kin `REQ-PUMP-STAGE-TRUTH` (the total-failure fingerprint and per-stage attribution, which instance 1 produced and this does not change).

### 7.62 A new wire record carries a `kind` tag — the untagged shape is already claimed, and an older daemon delivers it as chat  `[REQ-HAZARD-WIRE-UNTAGGED-RECORD]`

<!-- [doc->REQ-HAZARD-WIRE-UNTAGGED-RECORD] -->
- **Failure (caught in design, W4 T10 / releases#53 — before it shipped, by asking what an N-1 daemon actually does rather than assuming it would simply not understand):** the stream dispatcher classifies a feed by sniffing its FIRST LINE, and the message family is deliberately **untagged** — `classify_first_line` reads any first line carrying `target` + `op_id` with no `kind` as `StreamFamily::WanMsg`. A newly minted record type that omits a `kind` tag is therefore not "unrecognized" on an older peer: it is **misclassified as a message and delivered into the target endpoint's spool as chat**, on every N-1 node in the fleet, silently and with no error anywhere. The knock record hit this squarely — it necessarily names a `target`, so untagged it would have been claimed by that arm. The class is invisible to same-version testing by construction: both ends of a same-version pair agree, and only a cross-version reading of the classifier finds it.
- **Invariant:** every wire record minted from here on carries a `kind` tag routed by its own tagged arm, so an older daemon's `Some(_)` catch-all resolves it to `StreamFamily::Unknown` and DROPS it cleanly. The correct degrade for a request an old peer cannot serve is that it **never arrives** — never that it arrives as something else. A record whose absence is silent is recoverable; a record delivered as the wrong kind is not.
- **Proof obligation — three parts, or the pin goes vacuous:** (a) this version routes the record to its own family, AND is asserted NOT to route it to the message family; (b) the same bytes under a tag this version does not know resolve to `Unknown`; (c) the untagged shape is asserted to genuinely BE claimed by the message arm. Without (c) the test passes for the wrong reason the day the record's fields drift such that it would no longer have collided — the guard would then be measuring nothing while still green, which is the 7.61 family applied to a cross-version pin.
- **spt-core mapping:** `spt-net/src/net/knockmsg.rs` (`KNOCK_KIND`, the tag on `KnockRecord`); `spt-daemon/src/dispatch.rs` `classify_first_line` (the tagged arm) + `StreamFamily::Unknown` (the clean drop); unit `a_knock_record_routes_here_and_drops_cleanly_on_an_n1_daemon` (all three parts, against the real classifier).
- **Source:** W4 knocking wave (releases#53 T10; doyle-ruled 2026-07-31 as a hazard CLASS rather than a one-off, since every future wire record faces the same trap).

---

### 7.63 A dedupe key is never narrower than the identity of the record it replaces — a shared id with two owners must not collapse to one  `[REQ-HAZARD-MUTUAL-PREAUTH-SIBLING]`

<!-- [doc->REQ-HAZARD-MUTUAL-PREAUTH-SIBLING] -->
- **Failure (DOORBELL, releases#83/#79 lane; the near-miss is the entry's point):** arming a mutual pre-authorization deliberately REPLACES rather than stacks, so that a re-knock cannot accumulate duplicate reverse grants. The dedupe is correct; its KEY is where the trap sits. One code id is legitimately held by **two** pre-authorizations with different owners — the minter's, armed at mint with an empty subject because no redeemer exists yet, and the redeemer's own, armed at redemption with the subject known. Keyed on `(kind, key_id)` alone, arming the second finds the first and overwrites it **in place**: one side of an agreed two-way silently ceases to exist, with no error, no refusal, and no second grant left to notice missing. The surviving side reads as entirely correct on its own inspection, which is what makes this a silent-loss class rather than a bug someone trips over. `(kind, key_id)` is also the spelling a reader reaches for first, because it is how every *other* seam here addresses a pre-authorization — the narrow key is the intuitive one, exactly as in 7.61.
- **Invariant:** a replace-in-place keyed lookup states the FULL identity of the record it replaces — here `(kind, key_id, owner)`. Where a key is deliberately NARROWER than a record's identity, that narrowness is a decision the seam states in words, and a test pins that two records sharing the narrow key both survive. Silent replacement is a forbidden outcome; refusing, stacking, or replacing are all defensible, but only if the seam chose one.
- **The asymmetry is deliberate — do not "fix" it into agreement:** `consume_redeemed` filters on `(kind, key_id)` *without* the owner precisely because one redemption must make BOTH siblings due at once, each resolving its own subject. `armed_mutual`/`consume_mutual` also omit the owner, and are correct to: they serve the knock keyspace, where only the knocker's single record ever exists. Three seams, two key widths, one rule — the width matches what the seam is addressing, not what its neighbour uses. A future reader who unifies them for symmetry reintroduces this hazard on the arming side.
- **spt-core mapping:** `spt-store/src/knock.rs` `arm_mutual` (the owner-inclusive match, with the rule stated in its own doc comment) beside `consume_redeemed` (the owner-blind consumption that takes both siblings); unit `a_redemption_consumes_what_it_makes_due_and_nothing_else` — genuinely discriminating rather than merely adjacent, since dropping `owner` from the arming key makes its `due.len() == 2` assertion fail.
- **Source:** DOORBELL milestone (W2 `--mutual`/`--one-way` forced choice + W4 sealed-code redemption; doyle-ruled at the golden-head hand-off 2026-08-01 as a hazard CLASS — the trap is any dedupe whose key is narrower than its record's identity, and this milestone is simply where it was paid for). Kin 7.61 (the intuitive spelling being the one the fault satisfies).

---

### 7.64 Session-scoped authority does not survive a session-id rotation at rest — a boundary CARRIES it as a move, never leaves a copy  `[REQ-HAZARD-EMPOWER-SESSION-RESURRECT]`

<!-- [doc->REQ-HAZARD-EMPOWER-SESSION-RESURRECT] -->
- **Failure (LOCKSMITH t1, releases#117; MEASURED by red probe 2026-08-03, not inferred):** an engine-room empowerment is session-state — ADR-0052 says "nothing to revoke at rest" — and is filed under the session id the engine room's perch is bound to. `spt api boundary` rotates exactly that id, to an ARBITRARY `--to-session-id` past a guard that refuses only psyche-custody squats, and it is NEITHER of the two revocation edges ADR-0052 §3a names. The record was therefore left filed under the OLD session id: rotate to a session, empower, rotate away, DROP the authority at the new session, rotate BACK onto the old id — and the seat holds the subnet again with no admin code proved since the drop. The module's own doc comment asserted the opposite ("a stale file is unreachable by construction"; "if this sweep never ran, no caller would gain a single grant"), which is a claim about the resolver's INPUT and is false while the input is caller-supplied; it is retracted at both sites where it was said.
- **THE SCOPE IS AUTHORITY OUTLIVING ITS SESSION, NOT A STRANGER GAINING AUTHORITY.** `api boundary` is auth-gated to the perch's own harness, so the actor re-acquiring is the actor that held the grant. Do not let this be re-told as privilege escalation — the mis-telling makes the fix look optional.
- **Invariant:** a session-id rotation CARRIES the authority to the new id as a **MOVE**. When the rotation returns, the old session's record no longer grants. Two records for one grant is the same leak wearing a different hat. It **fails closed**: if the move cannot complete the authority is DROPPED with a loud diagnostic and never left at the old id as a fallback — so the property is that after any path (success, partial failure, crash between steps) authority is reachable at AT MOST ONE session id, and on a failure path at NEITHER. The order is what buys that: neutralize the old record FIRST, then write the new one; write-new-then-remove-old has a window in which both stand and the old one is the resurrection material — a fix with a state in which it reproduces its own bug.
- **Two barriers, because they fail for different reasons:** removal needs write permission on the session DIRECTORY, emptying-in-place needs it on the FILE. The second barrier deliberately trades the one-representation-of-nothing property (a record that GRANTS is strictly worse than one that is merely ambiguous, and an absent-or-corrupt record already loads as empty) — the trade is argued at `drop_all`'s doc, not made silently.
- **Gate the carry on the endpoint that can hold one.** Empowerment files are keyed by SESSION ID ALONE, not by endpoint, and nothing structurally stops another perch from carrying the engine room's sid — so an UNGATED carry lets an unrelated endpoint's boundary move the seat's live grant out from under it. That is a new defect introduced by the fix, and it needs its own row: one test that the gate is not too tight, one that it is not too loose.
- **SCOPE — this closes the BOUNDARY edge ONLY, and the class is NOT closed.** Two further mechanisms rotate a bound session id and leave the prior record standing: the dead-owner re-pin (`api/auth.rs`) and an `establish_perch` re-bind. Their polarity is the OPPOSITE — a boundary is one session lineage continuing (carry), those are a NEW session taking over a perch whose prior owner is gone, so the authority DROPS. Filed as **releases#138**; a reader who finds them here has landed on a filed item, not a discovery.
- **spt-core mapping:** `spt-store/src/empower.rs` `carry_session`/`carry_between`/`neutralize_at` (the move, the two barriers, and the injectable removal seam the failure arm is only reachable through); `spt/src/api/reporting.rs` `cmd_boundary` (the ER-gated call, the old sid captured under the SAME lock as the rotation, and the loud lines — it never fails the boundary, because the harness has already rotated and a refusal would desync the record from the reality it describes); ADR-0052 §3c. Units: `a_rotated_away_session_id_cannot_resurrect_a_dropped_empowerment` (whose discriminating step is DROPPING the authority at the new session before rotating back — without it a plain rotate-away/rotate-back cannot separate resurrection from the ruled carry), `a_non_engine_room_boundary_never_moves_the_seats_empowerment`, `a_removal_failure_still_stops_the_old_record_from_granting`, `a_failed_carry_drops_the_authority_rather_than_leaving_it_behind`.
- **Source:** LOCKSMITH tranche 1 (releases#117; doyle-ruled 2026-08-03 — carry-as-move, fail-closed, a NEW third named ADR edge rather than a stretch of the existing text, no widening of the hygiene sweep). Kin 7.51 (a recycled identity must not read as the original), 7.63 (a key narrower than the identity of what it addresses).

---

### 7.65 Identity is never resolved by directory enumeration order — an ambiguous ancestor is REFUSED by name, not broken by whichever perch `read_dir` yielded first  `[REQ-HAZARD-SELF-DETECT-TIE]`

<!-- [doc->REQ-HAZARD-SELF-DETECT-TIE] -->
- **Failure (paid-for, releases#240 / CONDUIT #236 respin; MEASURED on both platforms 2026-08-29, not inferred):** self-detect leg (c) (`roster::detect_self_by_ancestry`) built its candidate list from `read_dir(owlery)` and took the FIRST candidate whose recorded pid was the nearest ancestor. Nearest-first correctly answered the ancestry dimension and left a second one open: when two or more DISTINCT top-level perches record the SAME live pid, they all match at that ancestor, and the winner was whatever the directory happened to yield. From ONE on-disk state at ONE sha, ext4 returned `[(bob, P), (alice, P)]` and chose `bob` while NTFS returned `[(alice, P), (bob, P)]` and chose `alice`. `spt send --from alice` then published `MSG_OUT` with `owner=bob`, and the sink filed the sender's row into the RECEIVER's log exactly as `IoLogSink` is specified to do — the sink was innocent, it was handed the wrong owner.
- **THE GREEN WAS THE COIN LANDING HEADS.** The same cells passed on Windows in a gate run, a golden run, and a discarded head, and every one of those greens was enumeration order, not the code being right. No Windows pass of an identity-resolution cell is evidence about this seam. The order is arbitrary PER DIRECTORY — the same filesystem produced both orders in one session — so it is neither alphabetical nor creation-ordered and no platform is the safe one.
- **The ambiguous state is PRODUCTION-REACHABLE, established by construction rather than by not having seen it:** two `spt api bind` calls from ONE parent shell both succeed, with no refusal and no override flag, and both top-level `info.json` records carry that shell as `parent_pid` — the E-1 candidate arm (7.34 / `REQ-SELF-DETECT-PARENT-PID`). Nested Psyche/Worker perches are NOT a route: they live at `owlery/<parent>/nested/<child>/` and leg (c) scans top level only, so they are outside the candidate set by path topology.
- **Invariant:** when the candidates at the nearest matching ancestor name two or more DISTINCT ids, resolution is `None` with a loud-once diagnostic naming the tied ids and the shared pid — never a silent pick. Two candidates carrying the SAME id are NOT a tie (one spt-hosted perch matches on both its dead `rec.pid` and its live `parent_pid`; refusing that would re-break `REQ-SELF-DETECT-PARENT-PID`). The refusal is scoped to the nearest matching ancestor and does not fall through to an outer one — an outer perch is a DIFFERENT session, and answering with it is worse than declining. `None` is not a shrug: at the send site the owner falls back to the CALLED `--from`, so refusing is more ACCURATE than the coin as well as more honest. Label-only, like the whole of leg (c) — `authenticate()` is untouched (KH 7.3/7.5).
- **A rig that manufactures an unreachable state is how this reached a golden run.** The integration cells bound BOTH endpoints under the test process's own pid, a state production reaches only by the shell shape above; they now bind DISTINCT LIVE owners by construction (the author under this process, the peer under the daemon), so the tie cannot arise and the assertions are FORCED by the edges resolving correctly rather than permitted by enumeration order. The proof that it is forced and not re-lucky: after the fix the peer still enumerates FIRST on ext4 and the author still wins.
- **spt-core mapping:** `spt/src/roster.rs` `match_self_by_ancestry` (distinct-id refusal) + `warn_ambiguous_ancestor_once`; `spt/src/cli.rs` the send site whose `unwrap_or(--from)` fallback makes `None` the accurate answer; `spt/tests/io_events_undriven_kinds_e2e.rs` `bind_endpoint`/`rig` (distinct live owners). Units: `an_ambiguous_ancestor_resolves_to_none_in_either_candidate_order`, `one_perch_matching_on_both_its_pids_still_resolves`, `ambiguity_is_scoped_to_the_nearest_ancestor`.
- **Source:** releases#240, filed off the CONDUIT #236 respin RCA (todlando instrument on kitsubito + Windows control on hfenduleam; doyle-ruled fix shape — a named refusal over a silent guess; deployah pre-registered the ship branches before the classification landed). Kin 7.34 (the dead `rec.pid` class this ambiguity rides on), 7.63 (a key narrower than the identity of what it addresses).

---

### 7.66 A bounded retry wait that gates an assertion records its MARGIN on success and declares its expiry CENSORED on failure — a pass/fail loop is not a meter  `[REQ-HAZARD-RETRY-MARGIN-UNMEASURED]`

<!-- [doc->REQ-HAZARD-RETRY-MARGIN-UNMEASURED] -->
- **Failure (paid-for, releases#294 / v0.69.0: gate runs `34445961595` and `34481993681`, two reds at the SAME cell, `spt-daemon::sync two_tier_sync_lands_and_gate_refuses_server_side`):** the wait that gates the assertion is the house-standard 400 iterations x 25 ms — a 10.0 s budget — and it reports PASS or FAIL and nothing else. The elapsed is discarded. Two full gate arms across two boxes were spent and the cause of the red is still **UNCLASSIFIED**, because the instrument could not answer a question in either direction.
- **Defect (a) — a green carries no margin.** Instrumented at 400 observations on the Windows cell, successes landed at **4.448 s** (observation 176/400) and **9.940 s** (observation 392/400). Those are the SAME GREEN to the loop. A pass one scheduling hiccup from red is indistinguishable from a healthy one, so **a history of passes is not evidence of headroom** — every prior green of this cell is silent about margin, including the greens on the record head.
- **Defect (b) — an expiry is RIGHT-CENSORED, not a sample.** The loop stops looking at the bound (measured 10.156–10.200 s wall), so it establishes that no eligible condition was observed BEFORE expiry and **nothing whatsoever about after**. A censored expiry has **no upper endpoint**: it cannot be averaged in, cannot be called a latency or "the tail", and cannot separate DELAYED from ABSENT, RETIRED, or FILTERED. Write `CENSORED at X, no upper endpoint`.
- **THIS IS THE SEAM THAT WITHDREW THREE CAUSE PHRASINGS IN ONE HOUR** (2026-09-10, all mine): *"delayed publication, measured"* — censoring, caught by hertz; *"a ceiling outside the measured tail"* — unspecifiable, hertz **refused to implement rather than invent a number**, and that refusal was worth more than compliance; *"boundary defect MEASURED"* — caught by todlando, **proximity to a threshold is a RISK SIGNAL, never a verdict that the threshold is wrong**. If the 9.940 s success is itself pathological, the budget is innocent. An unmeasured loop cannot tell those apart, which is exactly why the budget was **not** changed.
- **Invariant:** (1) a gating retry wait records **elapsed-at-success against its budget** and surfaces it on the reporting path — never a bare `passed`; (2) an expiry is reported as **CENSORED AT `<bound>`, NO UPPER ENDPOINT**, never as a sample; (3) where the eventual outcome matters, expiry starts a **separately bounded diagnostic continuation that PRESERVES the original failure** — a continuation never converts the original failure into a pass — reporting first-observed elapsed, or an **INCOMPLETE OBSERVATION** rather than an absence if it too is bounded out; (4) an interval is named for **what was actually hooked** — *submission-to-registration* is not *wire latency*, and a window spanning process spawns and filesystem work is not a *publication* meter.
- **Why the naming clause is part of the hazard, not an aside:** the waited window at this site is COMPOSITE by construction — `request_sync_on` opens and subscribes, then runs `git rev-parse` once per requested ref BEFORE `net_stream_send`, and a first pull requests four refs while creating the requester's fresh bare Git store inside the timed overlap; QUIC's `accept_bi` becomes visible only on the peer's first write (`nethost.rs:1071-1089`). Calling that number a publication latency — as I did — attributes a composite of git init, four subprocesses, a first write and a visibility gap to one of its parts.
- **spt-core mapping:** measured site `crates/spt-daemon/tests/sync.rs:109`. **SCOPE GUARD — the 400x25 ms shape is house-wide** (`access.rs:80`, `attach.rs:114`, `broker.rs:618`, ~20 more): a lane fixes **only the measured site** and says so in the commit. A blanket rewrite of every call site is a separate, argued change, not a rider.
- **Source:** releases#294 gate triage (doyle ruled; hertz instrument design — the 400-iteration acceptance boundary kept, continuation added, **upheld over my wider-ceiling version**; todlando source-read mechanism candidate and the proximity correction; deployah `.spt/DIAG-ARM-CRITERIA-294.md`). Kin 7.61 (a monitor's green producible by the fault it watches — same family: a green that is not evidence).

---

## Conformance checklist (condensed)

| # | 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 from recorded status and a not-provably-gone PID/birth pair. Abrupt death and daemon-stop leave stale `online` records; derive truth at gates/renders and heal each changed record once. The same-boot no-spontaneous-relaunch rule applies only to nonpersistent shells. Persistent instances follow 2.7 regardless of launch age. Modern PID and birth are published and read as one atomic identity; failed replacement preserves the old evidence, and a cleared identity cannot resurrect its numeric mirror | `spt_store::shellinfo::{read_shell_process,record_shell_launch,clear_shell_pid,shell_pid_provably_dead,effective_status,is_shell_online}`, `linkhost`, `shelldisc`, `activity` |
<!-- [doc->REQ-HAZARD-SHELL-STALE-ONLINE] -->
| 2.7 | A once-per-generation boot sweep and each owner offline->online edge restore every instance whose adapter is `persistent`, owner is online, and binary is down in fact. Machine boot time, launch age, and absent launch stamps do not block persistent restoration; already-online and live binding launches are excluded. Heal every stale instance per owner with no repeat writes. Prove the restart shape without first suspending the fixture; a clean derived display alone is not evidence the record was healed | `shellwake::{heal_stale_online_records,restore_persistent_shells_at_boot,restore_persistent_shells_on_owner_online,OwnerOnlineEdge}`, `shellhost::live_launch_winner` |
| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
| 3.4 | A ring never adopts (so never deletes) a perch it did not create — probe the DIR, not the ready marker; record/spool/unreadable = occupied, empty = refused too; deliver + loud `RING_PERCH_EXISTS`/`RING_STALE_DIR` instead of block-waiting | `spt_msg::ring` probe + `create_dir` leaf |
| 3.5 | A perch GC classifies on record PRESENCE only and never asks a liveness resolver — `is_perch_alive` is INVERTED here (recordless residue reads ALIVE, an offline endpoint reads DEAD: 24 vs 6 measured on HFENDULEAM 2026-08-04), so `!is_perch_alive` authorizes exactly the inverse set; the registry is never asked either (offline endpoints are legitimately absent from it) | `spt_store::perchgc::sweep`, `spt endpoint gc` |
| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
| 4.1 | Envelope decode order, `&amp;` last | spt-proto (public, wire-versioned) |
| 4.2 | Parser panic-free + tolerant | spt-proto |
| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |
| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |
| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
| 5.3 | Timeout every harness subprocess | manifest invocations |
| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
| 7.7 | A wedged viewer is evicted, never stalls the controller/child/drain | broker `OutputLog` fan-out (controller/viewer) |
| 5.6 | Detached long-lived children inherit only ENUMERATED handles — `bInheritHandles=FALSE`, or TRUE solely beside a `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` allowlist (never bare) | daemon + shell spawn + `[service]` startup capture |
| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |
| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |
| 5.17 | Spawn-based test rigs use `cmd`/`sh`, never `powershell` (silent no-op under `DETACHED_PROCESS` — the flag alone is the cause, and cmd's PIPED stdout is lost under it too); descendants found by parentage via `proc::process_table()` | every rig over `daemon::detached_no_inherit*` |
| 5.19 | One process-global env var is guarded by ONE lock per binary — a crate may keep its own home-scoping wrapper, never its own mutex over a variable another helper in the same binary also writes (two locks serialize their own users and guard nothing between them; invisible under nextest and when run alone) | `spt_test_support::env_lock` ← `spt_daemon::test_home`, `spt::testutil::isolated_home` |
| 5.18 | A rig leaves no surviving descendant whose cwd is inside the repo tree (cargo hands every test child the PACKAGE ROOT; a survivor — child or grandchild — pins it undeletable while files inside stay writable). The pin is the descendant, never its `CREATE_NO_WINDOW` conhost — but a `--headless` ConPTY conhost inherits its creator's cwd and CAN be the sole pin; read the holder's identity, and a conhost's COMMAND LINE, before acting (orphan → kill, a live agent's shell → `cd` out) | `.github/ci/find-cwd-holders.ps1` (diagnosis), rig teardown completeness (prevention) |
| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |
| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |
| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |
| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |
| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |
| 6.12 | The echo-commune brief never transits the agent's `<id>-commune.md` (one writer on that path — the agent); the echo routes direct into the two-tier store | echo-commune seam / context tiering (releases#276) |
| 6.13 | IO event tail reads cannot mint false zero; append exceeds the global max, reset repair preserves file order, and trim retains by position | `spt_store::iolog` |
| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |
| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |
| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |
| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |
| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |
| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not "failed to fill whole buffer"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |
| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent = follow-up) | `daemon.rs::launch_daemon_job_neutral` (ladder), `spawn_daemon_via_wmi`/`wrapped_daemon_command`, int `job_escape_e2e.rs` |
| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |
| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |
| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |
| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |
| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |
| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |
| 7.17 | PTY **input** is single-writer: each session spawns ONE dedicated input-writer thread = the SOLE caller of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`). Every caller (`dispatch_input`, `dispatch_endpoint_input`, the inject worker/floor flush) ENQUEUES (`try_send`) + returns at once — a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. Full queue ⇒ DROP excess + stamp the perch `input_backpressure` (heal-on-resume: cleared on the next accepted enqueue); the daemon NEVER wedges on a stuck harness. Completes the W1b-deferred fix (2) of 7.14. The OUTPUT-side single-writer (7.12) mirror, applied to input | broker `InputWriter`/`input_writer`/`flush_inject_floor`/`run_inject_worker`/`dispatch_input`, `spt_store::info::set_input_backpressure`, int `inject_control_wedge.rs` |
| 7.18 | `spt rc` paste is **client-originated** on Windows: CC runs daemon-side with no access to the operator's LOCAL clipboard. On a RIGHT-CLICK rc reads the local clipboard itself and injects a BRACKETED paste (`ESC[200~` + content + `ESC[201~`) — CC has bracketed-paste mode on (`ESC[?2004h`), so a multi-line paste lands intact with NO `\r` submit-storm, harness-agnostic, content VERBATIM. RawGuard captures the mouse (disables console QuickEdit so right-click reaches the app) on an interactive console only + restores on drop. **ctrl+V is NOT intercepted** (P1b): WT consumes it (Key RELEASE only, never Press) + injects the clipboard as a key flood — it rides WT's native paste as keystrokes (no wedge, 7.19; bracketed = right-click). cfg(windows) only — Unix pastes natively | `rc.rs` `wrap_bracketed_paste`/`mouse_is_paste`/`clipboard_paste`/`read_clipboard`/`RawGuard`/`spawn_stdin_reader_events`, unit-only (live clipboard+mouse = HITL) |
| 7.19 | An operator input FLOOD must not deadlock the broker: `serve_attach` sends N `Input` frames on one conn without reading, the broker acks each on the SAME conn → the return direction fills (~pipe buffer) → `send_frame` blocks the per-conn handler → mutual full-duplex DEADLOCK (permanent broker wedge, controller latched). FIX: opt-in ack — `InputReq.ack` (serde default true); the operator/rc path sends `ack=false` (`send_effect_no_ack`), `dispatch_input` skips the applied frame → the handler never writes back while draining the flood. `shellchan` keeps `ack=true` (its `Applied`-wait). Exactly-once unaffected (dedup at the applied-set). N-1: an old resident broker still acks until restart (7.9 class) | `msg.rs` `InputReq.ack`, `brain.rs` `send_effect_no_ack`, `attach.rs` `serve_attach`, `broker.rs` `dispatch_input`, int (flood repro) |
| 7.20 | `spt rc` must forward the SCROLL wheel to the harness — our mouse capture (for right-click paste, 7.18) steals WT's native scroll. Track the harness's mouse-reporting mode from its OUTPUT (DECSET `ESC[?1000/1002/1003h` + `1006h` SGR, and `…l` off; scan survives a split across output chunks) into `MouseMode{enabled,sgr}`; forward `ScrollUp/Down` as an xterm SGR report (`ESC[<64/65;col+1;row+1M`) ONLY when `enabled && sgr`, else drop; Moved/drag/clicks dropped. cfg(windows) only — Unix scrolls natively | `rc.rs` `MouseMode`/`MouseModeScanner`/`scroll_dir`/`scroll_sgr`/`spawn_stdin_reader_events`, unit-only (live mouse = HITL) |
| 7.21 | Exactly ONE LIVE `controller_writer` per brain↔broker connection + every writer emits an ASCENDING seq stream ⇒ a snap-above consumer over the surviving writer's COMPLETE `[0,end]` replay delivers `[K,end]` with no skip/dup. A brain-restart re-serve double-registered the controller (handoff's `subscribe(from_seq=K)` + the re-serve's `attach_as(sid,0)`) → two writers raced one socket → the consumer's strict reject-gap legacy path saw `got seq 1 want 0` (flaky `attach_survives_target_brain_restart_exactly_once`). FIX (fix #1 "drop handoff's subscribe" REVERTED — it's the standalone-resume mechanism): (1) `handoff` seeds `session_cursors` → dedup-below + snap-above (correctness, made complete by the ascending-merge property); (2) `controller_writer` epoch-gated via shared `Arc<AtomicU64>` `controller_epoch`, re-read UNDER `send.lock()` (single live writer); (3) `subscribe_with` resets the resume-mode dedup cursor to `from_seq` (shared by `attach`/`attach_as`) — the LOAD-BEARING fix for the operator-stream boundary: serve_attach consumes the handoff replay's seq K before `attached`, advancing the cursor; the `attach_as(0)` re-subscribe reset re-delivers it so the operator viewport stays gapless. Pre-existing; P1b innocent | `brain.rs` `handoff`/`subscribe_with`, `broker.rs` `become_controller`/`controller_writer`/`controller_epoch`, unit `src/broker.rs`, int `tests/broker.rs`+`attach.rs` (keystone + restart carrier, 20× on kitsubito) |
| 7.22 | Endpoint-stop / brain-death reaps a brain-LESS perch's orphan detached Psyche via the cmdline-scoped guard — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT (the owning brain is dead, so its `psyche_child` handle died with it) and the brain-START sweep (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host runs the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Fail-safe-decline (pid-alive AND basename==psyche-program AND cmdline contains `<id>-psyche`; any unreadable signal DECLINES — a missed dup is bounded, a wrong-kill catastrophic). The orphan-leak half of perri F-010xF-015 (the psyche own-copy is the other half, ADR-0025 amendment) | `livehost.rs` `reap_orphan_psyche_for`/`reap_stopped_endpoint_orphan_psyche`/`reconcile_once`/`confirm_residency_or_unhost`, unit `livehost.rs`, int W3e (perri step 3) |
<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->
| 7.23 | An already-spooled message (WAN-arrived or spooled-while-active) NEVER depends on adapter hook-poll cadence to reach a relay-less spt-hosted endpoint — the daemon drives delivery on the events it owns. TWO daemon triggers feed ONE shared translation-binary inject leg (`spt_daemon::inject::try_spt_hosted_inject`): WAN ingress (`receive_wan` injects before spool — leg 1, `[REQ-WAN-SPT-HOSTED-DELIVERY]`) + the ACTIVE→IDLE edge (`drain_idle_window` claims the pending spool — NON-DEFERRED ONLY since the 2026-07-27 scope amendment; `active_only` is hook-carried and outside the guarantee — and injects, reusing the hook-poll take/ack so a concurrent `api poll` can't double-deliver — leg 2, `[REQ-MSG-IDLE-EDGE-DRAIN]`). v0.14.3 LAW holds on both: no binary ⇒ SPOOL LOUD (row released intact), never a raw PTY write. Fixes F-023 "sent but never lands" on an idle relay-less perch with a healthy binary | `inject.rs` `try_spt_hosted_inject`, `wan.rs` `receive_wan`, `delivery.rs` `drain_idle_window` + `spool.rs` `claim_idle_edge_at`/`release_at`, int `inject_control_wedge.rs` `wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` + `idle_edge_drain_e2e.rs` `spool_while_active_then_idle_fires_injection` |
<!-- [doc->REQ-HAZARD-DELIVERY-STARVATION] -->
| 7.24 | A DELEGATED live adapter apply never reports success without swapping, and the live-update seam uses ONE parent-aware matcher. `spt_runtime::profile::adapter_parent_matches(session_adapter, parent)` (`split_option().0 == parent`) at ALL three comparators (CLI `adapter_has_live_endpoint`, broker `dispatch_adapter_apply` filter, `select_endpoints_running_adapter`) — no exact `==` against a record name, so a `--adapter cc:ccs` composite endpoint resolves to parent `cc`. `dispatch_adapter_apply` swaps UNCONDITIONALLY (empty-affected early-return removed); `KIND_APPLIED` only after a real swap. Fixes F015B silent no-op (D1 matcher skew dropped every `:profile` endpoint → affected=[] → D2 success-without-swap) | `profile.rs` `adapter_parent_matches`, `broker.rs` `dispatch_adapter_apply`/`select_endpoints_running_adapter`, `cli.rs` `adapter_has_live_endpoint`, unit `profile.rs`+`broker.rs`, int (e2e a/b/c, this wave) |
<!-- [doc->REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP] -->
| 7.25 | A perch pinned to a DEAD session self-heals; a LIVE-owner rotation still refuses. `authenticate()` (auth.rs): sid mismatch AND recorded owner pid DEAD (`proc::is_process_alive`) → accept caller sid + RE-PIN (`mutate_info` rotate `session_id` + LOUD `SESSION_REPIN`) — ADR-0032 layer 2, same trust as `establish_perch`'s dead-owner rebind. LIVE-owner mismatch (a `/clear`/`/compact` live-pid rotation) STILL refuses (squat, unwidened — live-rotation proof is the adapter's layer-1 job, `[REQ-BOUNDARY-ROTATION-CREDENTIAL]`; parent_pid ancestry = parked layer 3). ADDITIVE to token auth (branch fires only on no/failed token AND sid mismatch AND owner dead). Fixes the F-024C/D permanent-strand wedge (one lost boundary rotation → every id-scoped hook incl boundary AUTH_REFUSED forever, stderr-silent in a hook) | `auth.rs` `authenticate` dead-owner branch, unit `auth.rs` `pinned_to_dead_sid_mismatched_poll_repins`+`live_owner_mismatch_still_refuses`+`token_auth_path_unchanged` |
<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->
| 7.26 | Concurrent first-touch of ONE fresh BranchStore all-Ok — the non-atomic `git init` (template-hook copy) + `config.lock` race must never strand a first-toucher. `open_or_init` → `init_bare_tolerant` (bounded backoff-retry on the init collision that desyncs racers + open-after-lose on a racer-created HEAD) + `config_set_locked_retry` (bounded `config.lock`-aware retry; `core.autocrlf` idempotent). Convergence guaranteed: a lone re-init on a partial dir completes. Fixes the G3-gate pump.rs:442 flake | `branchstore.rs` `open_or_init`/`init_bare_tolerant`/`config_set_locked_retry`, unit `concurrent_open_or_init_on_one_fresh_store_all_ok` (N=8, RED-FIRST) |
| 7.27 | A control/viewer stamp never outlives its session — every teardown path clears what attach stamped. `/exit` kills the CHILD (endpoint suspends on child death) so the teardown runs the REAP path (exit-waiter → `sessions.remove`), never the controller-detach path — the only pre-fix stamp-clear. The exit-waiter now calls `OutputLog::stamp_reaped()` = `set_driven_by(None)`+`set_controlled(false)`+`set_viewer_count(0)` on reap (broker single-writer, unconditional/idempotent, race-free — dead session ⇒ no concurrent re-stamp). Fixes F-026 #2 (hall-a latched `controlled:true` + gossiped `controller_node=self` for hours after `/exit`). Distinct from 7.15 (sessionless reconcile heal) | `broker.rs` `OutputLog::stamp_reaped` + exit-waiter reap call, int `control_stamp_lifetime.rs` `reap_clears_control_and_viewer_stamps` (RED-first) |
| 7.28 | A relative manifest path naming an endpoint-read/written location resolves against the ENDPOINT's cwd (`info.cwd`, read at USE time), never the daemon's process cwd (one daemon hosts many cwds). commune_dir/signoff_dir resolve at ingest: absolute as-is, relative+cwd joined, relative+no-cwd SKIPPED loud-once. Project routing spt-internal-gated (an anchor under `$SPT_HOME` → no `p-<project>`, live slice still commits; else `project_id_for_dir` parity). Owlery-gated until releases#178 widened the base to the whole spt home — a strict superset, so the parity leg is unchanged for every real user project. {cwd}-templated (digest/history) + daemon-absolute-filled (session-role/translation) keys already anchored. Fixes F-026 SI-1 (pristine BranchStore box-wide; abs-only test fixtures masked the relative leg) | `lifecycle.rs` `resolve_endpoint_drop_dir`/`is_spt_internal`/`warn_no_cwd_once`/`pulse_tick`, `ingest.rs` `route_slices` empty-id skip, int `relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch` (RED-first) |
| 7.29 | Control/viewer stamps CONVERGE to broker session-table truth (upward), not merely edge-trigger — the companion to 7.27's downward edge-clear. A picker-created endpoint's spawn `set_controlled(true)` fires PRE-bind (no perch → `mutate_info` NotFound swallowed); the adapter binds `controlled:false` and no edge re-stamps → uncontrolled-forever-while-driven (hall-b). Broker (single-writer) re-asserts each live session's stamps DIVERGENCE-GATED (write only on diff, no fsync storm) on the `KIND_SESSIONS` poll (bounded window = reconcile cadence, no new timer); truth snapshotted under the log lock, writes OFF it. Event-on-input insufficient (idle controlled sessions). Fixes F-026 stamp-gap | `broker.rs` `stamp_divergence`/`converge_perch_stamps`/`has_controller`/`live_viewer_count` + KIND_SESSIONS handler, unit `stamp_divergence_gates_writes`, int `converge_stamps_on_sessions_poll_after_late_bind` (RED-first) |
| 7.30 | A Psyche failure of ANY shape never removes/alters the parent endpoint's ready/hosted state — a Psyche is a bounded per-event turn, so there is no "resident gone" signal; a turn failure stamps `psyche_host_error` ONLY, never `status`/ready. Residency machinery (`confirm_residency_or_unhost`) + the teardown-on-psyche-trouble deleted. Fixes the adapter v0.13.2 field brick (shim exit → residency teardown → ready removed → permanent `cli-gate-not-hosted`) | `livehost.rs` residency/reap deletions, `lifecycle.rs` `run_psyche_event_turn` psyche-fields-only stamp + `first_turn_psyche_context` non-empty, int (hall-bf shape: failing psyche → parent stays deliverable, no churn) |
<!-- [doc->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION] -->
| 7.38 | Every physical broker-conn write bounded + cancelable + poison-on-failure (independent watchdog aborter; poisoned conn never reused; SendHalf never leaves the conn object) | broker `conn.rs` `BrokerConn`, all write sites (`controller_writer`/`viewer_writer`/`send_frame`/nethost), int `brain_decouple.rs` |
| 7.31 | The Psyche failure budget counts REAL per-event attempts — every bounded per-event turn feeds a consecutive-N budget (default 3), so per-event churn a resident rate-guard was blind to now counts by construction. Fixes hall-bf's ~12/min re-host churn that ran invisibly (boot records weren't ledger boundaries to the old guard) | `lifecycle.rs` `note_turn_outcome`/`psyche_turn_strikes_exhausted` (stamps `psyche_host_error` on exhaustion, no `status` de-stamp), unit synthetic failure-loop trips the budget (RED-first control) |
<!-- [doc->REQ-HAZARD-THRASH-GUARD-BLIND] -->
<!-- [doc->REQ-HAZARD-STORE-INIT-RACE] -->
| 7.64 | Session-scoped authority CARRIES across a session-id rotation as a MOVE, never a copy: after any path through the boundary it is reachable at at most ONE session id, and on a failure path at NEITHER (neutralize the old record BEFORE writing the new one; two barriers, removal then empty-in-place; loud, and never a boundary refusal). Gated on the endpoint that can hold one, since the record is keyed by session id ALONE. Closes the BOUNDARY edge only — the dead-owner re-pin and an `establish_perch` re-bind have the opposite polarity (DROP) and are filed as releases#138 | `empower::{carry_session,carry_between,neutralize_at}`, `api/reporting.rs` `cmd_boundary` (ER-gated, old sid captured under the rotation's own lock), ADR-0052 §3c |
<!-- [doc->REQ-HAZARD-EMPOWER-SESSION-RESURRECT] -->
