The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v041-linux-respawn-blocker.md has been updated successfully. (file state is current in your context — no need to Read it back)

**STATUS 2026-06-11:** v0.4.1 PUBLIC (counter 8). hfenduleam rolled GREEN (no-bounce + wedge-heal proven, Windows leg of restoration DONE). kitsubito left as-is (v0.4.0 brain, unwedged, applied:8 record left uncorrected = appendix evidence). enlyzeam untouched. **V042-PLAN.md written**, sent to doyle for SIGN-OFF (operator delegated post-plan greenlight to doyle). Operator chose **debug-channel rollout** for the final Linux seamless observation. Next: doyle signs → mint REQ + build (T1 doc → T2 impl → T3 unit → T4 int → T5 release) → deployah publishes counter 9 → kitsubito+enlyzeam one bounce each → debug rollout → restoration close.

**v0.4.2 plan (deployah+todlando aligned; doyle confirmed ruling):**

---
name: v041-linux-respawn-blocker
description: v0.4.1 fleet roll found a Linux brain-respawn-path bug; restoration close BLOCKED pending v0.4.2 fix
metadata:
  node_type: memory
  type: project
  originSessionId: f3f208d9-9c31-4aaa-acfc-98acbfdf2509
---

v0.4.1 (counter 8) PUBLISHED 2026-06-11 (deployah, PR #4 merged @ 038257b, tag v0.4.1). Fleet roll surfaced a **Linux seamless-update bug** → restoration close BLOCKED, v0.4.2 needed.

**hfenduleam (Windows): GREEN.** apply → brain pid 38324→6620, gen 0→1, broker pid 17232 UNCHANGED, exe_hash 81c24675(0.4.0)→728a1bfc(0.4.1 win artifact), pump STALLED 16199s→live. No-bounce PROVEN on Windows.

**kitsubito (Linux): RED.** apply recorded "applied v8" (applied-state phase:applied, no rollback), canonical `/home/reavus/.local/bin/spt` correctly swapped to 35c9544c (v0.4.1 linux artifact) — BUT respawned brain (pid 1247825, gen 1) execs `.local/bin/spt.old-8` = b4b7ea14 (v0.4.0). **Brain runs OLD bytes while recording success.**

**ROOT CAUSE (code-confirmed `brainproc.rs:815-818`):** candidate respawn `binary=None` → `std::env::current_exe()`. Linux `apply` renames running `spt`→`spt.old-N`, writes new bytes to `spt`; resident broker's `current_exe()` follows the rename to `.old-N`, so the new brain execs old bytes. Rollback path `Some(.old-N)` is fine; the NORMAL path is wrong. Windows dodges it (can't rename a running PE in place). `current_exe_hash()` (l.324) also reads current_exe → breadcrumb honestly showed old bytes; doyle's bytes-level exe_hash assert is what caught it (readiness gate passed on the old brain = false-success).

**E2E gap (`brain_survive.rs`):** models swap as a PATH-FLIP between two distinct fixtures (fixture_a→fixture_b), never an in-place rename — so `current_exe()`-follows-rename is never exercised; CI green on kitsubito while prod fails.

**v0.4.2 plan (deployah+todlando aligned; doyle confirming):**
- R1 mint `REQ-HAZARD-BRAIN-RESPAWN-PATH`: respawn must exec applied bytes via resolved canonical install path (captured at broker boot, pre-rename), never current_exe/.old-N.
- R2 tighten readiness-success: promotion asserts respawned exe_hash == applied artifact hash (close false-success hole).
- FIX brainproc.rs candidate spawn → canonical install path.
- TEST Linux in-place-rename apply E2E asserting respawned exe_hash == artifact.
- GATES + CI both runners → deployah publishes **counter 9**.
- FLEET: resident v0.4.0 brokers can't self-fix → v0.4.2 needs a **one-time MANUAL broker bounce on Linux** = project's actual FINAL manual bounce, paired with enlyzeam catch-up. THEN restoration close.

See [[v040-published-fleet-rolled]], [[pump-ipc-deadline-fix]].

-**v0.4.2 plan (deployah+todlando aligned; doyle confirming):**

+**STATUS 2026-06-11:** v0.4.1 PUBLIC (counter 8). hfenduleam rolled GREEN (no-bounce + wedge-heal proven, Windows leg of restoration DONE). kitsubito left as-is (v0.4.0 brain, unwedged, applied:8 record left uncorrected = appendix evidence). enlyzeam untouched. **V042-PLAN.md written**, sent to doyle for SIGN-OFF (operator delegated post-plan greenlight to doyle). Operator chose **debug-channel rollout** for the final Linux seamless observation. Next: doyle signs → mint REQ + build (T1 doc → T2 impl → T3 unit → T4 int → T5 release) → deployah publishes counter 9 → kitsubito+enlyzeam one bounce each → debug rollout → restoration close.

+**v0.4.2 plan (deployah+todlando aligned; doyle confirmed ruling):**


1	- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan
2	- [CI self-host migration](ci-selfhost-migration.md) — DONE: runners live on kitsubito + hfenduleam (gravity retired 2026-06-07); twohost jobs gated by [twohost] commit tag; don't re-plan
3	- [which process is the listener](spt-daemon-is-live-infra.md) — owl.exe (plugin) = live listener, hands off; target\debug\spt.exe daemon = dev/test artifact, killable
4	- [self-clear skill API-blocked](self-clear-skill-api-blocked.md) — never invoke /self-clear; usage-policy classifier refuses the turn; continue in-context / user-manual /clear instead

- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan
- [CI self-host migration](ci-selfhost-migration.md) — DONE: runners live on kitsubito + hfenduleam (gravity retired 2026-06-07); twohost jobs gated by [twohost] commit tag; don't re-plan
- [which process is the listener](spt-daemon-is-live-infra.md) — owl.exe (plugin) = live listener, hands off; target\debug\spt.exe daemon = dev/test artifact, killable
- [self-clear skill API-blocked](self-clear-skill-api-blocked.md) — never invoke /self-clear; usage-policy classifier refuses the turn; continue in-context / user-manual /clear instead


Edit

- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan

- [v0.4.1 Linux respawn blocker](v041-linux-respawn-blocker.md) — v0.4.1 PUBLIC (counter 8); fleet roll found Linux brain-respawn bug (current_exe follows apply-rename → brain runs OLD bytes, records applied:8). hfenduleam(Win) GREEN, kitsubito(Linux) RED. V042-PLAN.md written, awaiting doyle sign-off; debug-channel rollout = final Linux proof. Restoration close BLOCKED on v0.4.2. ACTIVE
- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\MEMORY.md has been updated successfully. (file state is current in your context — no need to Read it back)

- [v0.4.1 Linux respawn blocker](v041-linux-respawn-blocker.md) — v0.4.1 PUBLIC (counter 8); fleet roll found Linux brain-respawn bug (current_exe follows apply-rename → brain runs OLD bytes, records applied:8). hfenduleam(Win) GREEN, kitsubito(Linux) RED. V042-PLAN.md written, awaiting doyle sign-off; debug-channel rollout = final Linux proof. Restoration close BLOCKED on v0.4.2. ACTIVE
- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan

- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan
- [CI self-host migration](ci-selfhost-migration.md) — DONE: runners live on kitsubito + hfenduleam (gravity retired 2026-06-07); twohost jobs gated by [twohost] commit tag; don't re-plan
- [which process is the listener](spt-daemon-is-live-infra.md) — owl.exe (plugin) = live listener, hands off; target\debug\spt.exe daemon = dev/test artifact, killable
- [self-clear skill API-blocked](self-clear-skill-api-blocked.md) — never invoke /self-clear; usage-policy classifier refuses the turn; continue in-context / user-manual /clear instead
- [Adapter glue-model boundary](adapter-glue-model-boundary.md) — adapters = manifest + binary only; real adapters in own repos; no adapter-aware source in spt-core
- [M6 shipped v0.1.0](m6-shipped-v010.md) — v0.1.0 public 2026-06-05; release procedure = RELEASE-RUNBOOK.md; releases-repo/ is repo-face truth; don't re-plan M6
- [Gravity deploy procedure](gravity-deploy-procedure.md) — ssh david@100.100.62.43; build in ~/spt-core-deploy NOT runner workspace; setsid daemon; ~1 min first-sync after restart
- [Linux SPT_HOME no-bug](linux-spt-home-no-bug.md) — unix spawn_detached inherits env (no env_clear), forwards SPT_HOME; only Win CreateEnvironmentBlock rebuilds/drops it (f99dc25). Default-home daemon = launched w/o the var, not a propagation bug; don't reopen
- [kitsubito Linux rig](kitsubito-linux-rig.md) — ssh reavus@kitsubito (key auth); cargo at ~/.cargo/bin; deploy checkout origin=local runner (NOT GitHub) so unpushed commits transfer via git bundle; live spt-daemon user unit, --user ops need no sudo; build-in-scratch leaves ~/.local/bin/spt untouched
- [Use $OWL send for messaging](owl-send-not-legacy-spt-send.md) — `$OWL send` works (legacy = where live perches run); `spt send` is the NEW spt-core binary but no perches on it yet → bounces NO_PERCH; don't read `$?` after piping send through tail (use PIPESTATUS)
- [daemon service-detection gotcha](daemon-service-detection-gotcha.md) — service detection reading GLOBAL OS state is blind on a single-home non-elevated dev box; gate Linux service routing on SPT_HOME-unset, Windows run-refuse on would-vanish (not is_elevated); push→CI BEFORE tagging a release (v0.3.0 tagged early, CI caught 2 regressions, retagged)
- [v0.3.2 platform-safe update](v032-platform-safe-update.md) — REQ-UPD-8 PUBLISHED + rig-verified 2026-06-09 (counter 6): fetch stages multi-platform update-set, apply refuses unstamped/wrong-OS single. Fleet kitsubito+hfenduleam=0.3.2 (ENLYZEAM TODO). GOTCHA: fetch/pump floor=staged_version → can't re-stage same version single→set; clear `$SPT_HOME/releases` first (Linux ~/.spt-core, Win %LOCALAPPDATA%\spt-core). CORRECTION: release-publish never built a SignedUpdateSet pre-v0.3.2 (was debug_rollout)
- [boot-race self-heal](boot-race-self-heal.md) — v0.3.1: daemon self-heals a net-less boot start (NetHost retry+attach to OnceLock broker); systemd-USER network-online ordering does NOT reliably prevent the race under linger (field-proven kitsubito) — the self-heal is the real guarantee, unit ordering is belt
- [v0.4.0 published + fleet rolled](v040-published-fleet-rolled.md) — restoration release PUBLIC 2026-06-10 (counter 7); D7-4 bytes-match done both nodes (brain.ready exe_hash == artifact); v0.3.2→v0.4.0 apply needed last manual bounce (in-process can't self-relaunch); seamless no-bounce proof + enlyzeam catch-up + milestone close all deferred to v0.4.1
- [v0.4.1 published](v041-published.md) — PUBLIC 2026-06-11 (counter 8); deployah drove full merge→tag→sign→publish solo; D7 SEAMLESS no-bounce fleet-verify payload (apply must roll bytes live w/ pid change, no manual bounce); acceptance hashes linux 35c9544c… win 728a1bfc…; hfenduleam(Win) GREEN no-bounce, kitsubito(Linux) RED → CLOSE BLOCKED: v0.4.0 broker respawns via current_exe() which follows apply-rename to .old-8=OLD bytes (Linux only); artifacts SOUND don't yank; v0.4.2 fix = respawn by canonical path + bytes-assert, needs manual Linux broker bounce; deployah takes counter 9 publish

+- [v0.4.1 Linux respawn blocker](v041-linux-respawn-blocker.md) — v0.4.1 PUBLIC (counter 8); fleet roll found Linux brain-respawn bug (current_exe follows apply-rename → brain runs OLD bytes, records applied:8). hfenduleam(Win) GREEN, kitsubito(Linux) RED. V042-PLAN.md written, awaiting doyle sign-off; debug-channel rollout = final Linux proof. Restoration close BLOCKED on v0.4.2. ACTIVE


## Status — v0.4.2 plan written, awaiting doyle sign-off

**Done this session:**
- v0.4.1 (counter 8) cut, gated, honesty-fixed, CI green both runners, **published** by deployah
- Fleet roll: **hfenduleam (Windows) GREEN** — no-bounce + wedge-heal proven (the Windows leg of restoration is genuinely done)
- **kitsubito (Linux) RED** — found + root-caused the `current_exe`-follows-rename respawn bug; brain runs old bytes under a false `applied:8` record
- Both partners aligned (doyle ruling + deployah R2 belt), finding captured in memory + docs
- **`V042-PLAN.md` written** → sent to doyle for sign-off

**Operator decisions logged:** plan-first → doyle approves → build/vet/publish; **debug-channel rollout** for the final Linux observation.

**The v0.4.2 fix (two halves):** capture the canonical exe path once at broker start (gives Linux Windows' path-at-start semantics) + a promotion bytes-gate that auto-rolls-back on `exe_hash` ≠ artifact (kills the false-success hole). New `REQ-HAZARD-BRAIN-RESPAWN-PATH` (KH 6.11) + a real in-place-rename E2E to close the test gap.

**Blocked on:** doyle's review of `V042-PLAN.md`. On sign-off I mint the REQ and execute T1→T5 (deployah publishes counter 9; I may hand the rename-E2E to deployah). Restoration close stays open until the post-fix Linux debug rollout proves seamless.

Holding kitsubito/enlyzeam untouched. Awaiting doyle — their reply arrives on the event stream.


deployah is authorized to publish when it's ready. just message them


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="d1644560" type="msg" from="todlando">doyle SIGN-OFF — V042-PLAN.md APPROVED. GO: mint the REQ, cut the impl. Both explicit nods granted, one small binding addition.

NOD 1 — degrade-to-readiness on either-hash-absent: CORRECT N-1 stance. exe_hash is additive by design (D7-1: &quot;a reader tolerates its absence&quot;) and KH 6.8 binds pre-read</EVENT-PART>
<EVENT-PART seq="2/7" id="d1644560">y durable state to N-1-readable; fail-closed would brick legitimate pre-metadata trials and contradict the additive-field posture. BINDING ADDITION: the degrade must be LOUD — eprintln PROMOTE_BYTES_UNVERIFIED:{version}:{ready-hash-absent|artifact-hash-absent} at the degrade point. Without it, a future regression that stops exe_hash being wr</EVENT-PART>
<EVENT-PART seq="3/7" id="d1644560">itten would silently DE-ARM the gate forever; the loud line makes a disarmed gate field-diagnosable (same honesty pattern as the heartbeat). Cover it in the T3 truth table (absent-arm asserts the degrade outcome; the log line itself is best-effort).

NOD 2 — capture at spawn_brain_supervisor entry: CORRECT, no deeper point needed. Your</EVENT-PART>
<EVENT-PART seq="4/7" id="d1644560"> reasoning holds: apply can only rename AFTER the broker is up when daemon-driven, and an apply that ran while NO daemon was up leaves the canonical path holding the new bytes — a later boot captures canonical, which is right. The one true race (an apply rename landing in the microseconds between broker exec and the capture line) is real but</EVENT-PART>
<EVENT-PART seq="5/7" id="d1644560"> acceptable: vanishingly rare, requires racing apply against daemon boot, and Half 2 is the backstop — a wrong-bytes trial now rolls back loudly instead of promoting. Put that window + the backstop rationale in a one-line comment at the capture site so the next reader doesn't re-litigate it.

Plan-quality notes (no changes needed): sta</EVENT-PART>
<EVENT-PART seq="6/7" id="d1644560">ged_artifact_hash keyed by version correctly guards against gating vs a different staged release; the gate living in the Promoted arm scopes it to trials only (non-trial respawns untouched — right); T4's rename-a-running-binary works on both OSes (Windows allows it — production apply already does); the ADR-0018 Q3 amendment note in T1 is t</EVENT-PART>
<EVENT-PART seq="7/7" id="d1644560">he right call; bootstrapping note honest.

Same loop: per-task gates, ping me pre-merge with the green run. deployah building T4 is fine — coordinate so the int tag lands in the same change set as the activation (rule 5).</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


doyle **SIGNED OFF — GO.** Both nods + one binding addition: degrade-to-readiness must be **LOUD** (`PROMOTE_BYTES_UNVERIFIED:{version}:{which-hash-absent}`) so a future regression that silently de-arms the gate stays field-diagnosable; cover in T3 truth table. Plus a one-line comment at the capture site (the microsecond apply-vs-boot race + Half-2 backstop). Rule 5: int tag lands in the same change set as activation.

Building T4 solo (tightly coupled to impl + the activation; cleaner than coordinating). ACK doyle + deployah, branch, read REQ template.


Bash

$OWL send doyle 2>/dev/null <<'EOF'
ACK sign-off — building. Binding addition folded: degrade arm emits eprintln PROMOTE_BYTES_UNVERIFIED:{version}:{ready-hash-absent|artifact-hash-absent} (best-effort line), T3 absent-arm asserts the degrade OUTCOME. Capture-site one-liner: the microsecond apply-vs-boot race + Half-2 rollback backstop, won't re-litigate. Activating required_stages incrementally per commit (doc→impl→unit→int) so traceable stays green each commit AND int-activation lands with T4's int evidence (rule 5). I'll build T4 myself (coupled to the impl + activation) — keeps it in one change set. Pinging you pre-merge with the green CI run.
EOF
echo "d:${PIPESTATUS[0]}"
$OWL send deployah 2>/dev/null <<'EOF'
doyle signed V042-PLAN.md — building now. I'll take T4 (rename-E2E) myself — it's coupled to the impl + must land in the same change set as the int activation (rule 5), so solo is cleaner than coordinating the merge. You stay on the publish leg: counter 9 when I ping CI-green. Thanks for the offer. Will ping when ready to tag.
EOF
echo "p:${PIPESTATUS[0]}"
git checkout -b fix/v0.4.2-brain-respawn-path 2>&1 | tail -1
echo "=== REQ-HAZARD template (pump-ipc) ==="
sed -n '623,648p' traceable-reqs.toml

ACK both + branch + read REQ template


d:0
p:0
Switched to a new branch 'fix/v0.4.2-brain-respawn-path'
=== REQ-HAZARD template (pump-ipc) ===
id = "REQ-HAZARD-PUMP-IPC-DEADLINE"
title = "The single-threaded peer pump's brain-IPC reads are deadline-bounded (PUMP_PEER_IO_TIMEOUT, total-wait per call); a TimedOut read POISONS the client and escalates to a SUPERVISED RESTART, never a per-peer retry — a black-holed peer must never wedge the whole pump"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-11: deployed v0.4.0 pump wedged 2.2h on hfenduleam — net_open_stream's unbounded `loop { read_event }` blocked on a peer conn the broker never replied for; the single-threaded pump froze and supervise_pump cannot rescue a BLOCKED thread (it only catches panic/error/return). Surfaced twice (2026-06-07 + this 2.2h wedge); the stall-warning was the band-aid, not the fix. fix = Brain pump-mode carrier SPLIT at construction (Brain::cold_start_pump → BrainConn::Split: a `pump-ipc-reader` thread does blocking read_frame on the RecvHalf → channel; main thread writes the SendHalf and reads with Receiver::recv_timeout for a per-call total-wait deadline; deadline re-armed on stream progress for the pull legs). The reader-thread+channel mechanism (NOT a non-blocking socket + poll) is mandatory because interprocess 2.4.2 on Windows named pipes has no portable read timeout and its set_nonblocking corrupts mid-stream (mesh E2E proven). A TimedOut bubbles out of run_peer_pump → supervised restart (fresh brain client + conn cache + WorkerLasts re-prime, the V4 stagger, idempotent). The broker-side bound (broker must never make a brain wait unbounded on a QUIC op) is deferred to the next broker-update batch (DEFERRED.md). doc = KNOWN-HAZARDS entry; unit = (a) a never-replying broker over a real socket → Brain::cold_start_pump → net_open_stream returns TimedOut (never blocks) [tests/pumpdeadline.rs], (b) the recovery tier-split (TimedOut → bubble/Err = supervised restart; ordinary Err → per-peer abort only)
[[requirements]]
id = "REQ-HAZARD-SUDO-SECURE-PATH"
title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 field report (KITSUBITO): `spt subnet create` refused unelevated, user ran `sudo spt …` → `sudo: spt: command not found` (spt in ~/.local/bin, off secure_path). fix = elevation::rerun_command emits `sudo <abs-exe> <args>` (shell-quoted) + try_auto_elevate re-execs under sudo on interactive Unix; main.rs drops the confusing DEELEVATED line. unit = absolute-path-not-bare-name + shell-quoting + windows-none + should_auto_elevate truth table (the sudo exec itself needs a real TTY → manual/kitsubito verified)
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
[[requirements]]
id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
# --- Added 2026-06-09 from grill-with-docs (ADR-0018: broker/brain in-process-collapse regression); rule 3: register before satisfying, rule 5: inactive until the broker/brain split restoration milestone (next milestone) ---
[[requirements]]
id = "REQ-HAZARD-BROKER-PROCESS-ISOLATION"


REQ template clear. Reading the toml insertion point (near the restoration hazards) + KH summary table to write T1 (doc).


<owl_messages>
<owl_message from="deployah" priority="high">Makes sense — T4 rides the impl + int activation in one change set (rule 5), solo's cleaner than a cross-merge. I'm on the publish leg only: counter 9 the moment you ping CI-green both runners. Path's warm, seed resident. Go build. Idle.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


648	id = "REQ-HAZARD-BROKER-PROCESS-ISOLATION"
649	title = "Broker and brain are separate processes: the broker runs as its own long-lived per-machine process that survives every brain restart, so a routine (brain-only) self-update restarts the brain onto the swapped binary while every hosted endpoint (PTY child, live QUIC conn, listening socket) stays untouched at the PROCESS level. The in-process-thread broker (daemon.rs:165-170) is a regression that silently unrealizes REQ-UPD-3 — apply degrades to an in-process Brain::handoff no-op and new code does not run until an unrelated restart (KNOWN-HAZARDS 6.7). Evidence must prove process-level survival (SPIKE-01/03 productionized as int: PTY child + live QUIC survive a brain-PROCESS restart onto a swapped binary), re-pointing the regression-masked in-process int tags currently on REQ-DAEMON-2 / REQ-UPD-3 (ADR-0018)."
650	required_stages = ["doc", "impl", "unit", "int"]  # activated restoration D1 (ADR-0018 Q2/Q3): doc = KNOWN-HAZARDS 6.7 + ADR-0018 + design doc; impl = brainproc.rs (brain child entry + broker-side process supervisor) + daemon.rs spawn-brain-supervisor + the hidden `daemon brain` CLI entry; unit = brainproc supervisor respawn/backoff table + the real-binary brain_split.rs process-level smoke (broker survives a brain kill + respawns it). int ACTIVATED restoration D7 = the productionized SPIKE-01/03 process-level survival E2E (crates/spt/tests/brain_survive.rs: a PTY child + a live QUIC conn + held-AND-functional output survive a brain-PROCESS restart onto a swapped binary, exe_hash-proven new bytes — D7-1) + the new-brain x old-broker N-1 verb-surface gate (crates/spt/tests/n1_pairing.rs + ci.yml n1-gate, pinned old broker 0c95435 — D7-2). The D7-1 E2E also re-points the regression-masked in-process int tags off REQ-DAEMON-2 / REQ-UPD-3 (below)
651	[[requirements]]
652	id = "REQ-HAZARD-ROLLBACK-STATE-COMPAT"
653	title = "A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists; unmintable retroactively once a migration ships."
654	required_stages = ["doc", "impl", "unit"]  # doc activated restoration D1 (KNOWN-HAZARDS 6.8 + ADR-0018 + design doc V1). impl/unit activated restoration D6-3 (the one D6 toml activation): impl = rollback_compat.rs (the pre-ready durable-file registry PRE_READY_DURABLE_FILES — the enumerated guard surface) + the two-phase AppliedRecord (relcache.rs, additive durable state); unit = the rollback_compat tripwire (every current pre-ready file shape is additive / N-1-readable; a non-additive pre-ready change trips the assert). The two-phase record + the generation-stamped brain.ready are additive → N-1-safe by construction (KH 6.8 D6 guard note)
655	# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
656	[[requirements]]
657	id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
658	title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
659	required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
660	[[requirements]]
661	id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
662	title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
663	required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
664	# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
665	[[requirements]]
666	id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
667	title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
668	required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
669	[[requirements]]

id = "REQ-HAZARD-BROKER-PROCESS-ISOLATION"
title = "Broker and brain are separate processes: the broker runs as its own long-lived per-machine process that survives every brain restart, so a routine (brain-only) self-update restarts the brain onto the swapped binary while every hosted endpoint (PTY child, live QUIC conn, listening socket) stays untouched at the PROCESS level. The in-process-thread broker (daemon.rs:165-170) is a regression that silently unrealizes REQ-UPD-3 — apply degrades to an in-process Brain::handoff no-op and new code does not run until an unrelated restart (KNOWN-HAZARDS 6.7). Evidence must prove process-level survival (SPIKE-01/03 productionized as int: PTY child + live QUIC survive a brain-PROCESS restart onto a swapped binary), re-pointing the regression-masked in-process int tags currently on REQ-DAEMON-2 / REQ-UPD-3 (ADR-0018)."
required_stages = ["doc", "impl", "unit", "int"]  # activated restoration D1 (ADR-0018 Q2/Q3): doc = KNOWN-HAZARDS 6.7 + ADR-0018 + design doc; impl = brainproc.rs (brain child entry + broker-side process supervisor) + daemon.rs spawn-brain-supervisor + the hidden `daemon brain` CLI entry; unit = brainproc supervisor respawn/backoff table + the real-binary brain_split.rs process-level smoke (broker survives a brain kill + respawns it). int ACTIVATED restoration D7 = the productionized SPIKE-01/03 process-level survival E2E (crates/spt/tests/brain_survive.rs: a PTY child + a live QUIC conn + held-AND-functional output survive a brain-PROCESS restart onto a swapped binary, exe_hash-proven new bytes — D7-1) + the new-brain x old-broker N-1 verb-surface gate (crates/spt/tests/n1_pairing.rs + ci.yml n1-gate, pinned old broker 0c95435 — D7-2). The D7-1 E2E also re-points the regression-masked in-process int tags off REQ-DAEMON-2 / REQ-UPD-3 (below)
[[requirements]]
id = "REQ-HAZARD-ROLLBACK-STATE-COMPAT"
title = "A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists; unmintable retroactively once a migration ships."
required_stages = ["doc", "impl", "unit"]  # doc activated restoration D1 (KNOWN-HAZARDS 6.8 + ADR-0018 + design doc V1). impl/unit activated restoration D6-3 (the one D6 toml activation): impl = rollback_compat.rs (the pre-ready durable-file registry PRE_READY_DURABLE_FILES — the enumerated guard surface) + the two-phase AppliedRecord (relcache.rs, additive durable state); unit = the rollback_compat tripwire (every current pre-ready file shape is additive / N-1-readable; a non-additive pre-ready change trips the assert). The two-phase record + the generation-stamped brain.ready are additive → N-1-safe by construction (KH 6.8 D6 guard note)
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
[[requirements]]
id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
[[requirements]]
id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
[[requirements]]
id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
[[requirements]]


deployah idle on publish leg — proceeding. **T1 (doc).** Minting the REQ in `traceable-reqs.toml` (required_stages=["doc"] for this commit; bumped per stage).


Edit

# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---

# --- Added 2026-06-11 (v0.4.2): the v0.4.1 fleet-roll Linux brain-respawn blocker (ADR-0018 Q3 amendment); rule 3 register-before-satisfy, rule 5 activate per task ---
[[requirements]]
id = "REQ-HAZARD-BRAIN-RESPAWN-PATH"
title = "The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11."
required_stages = ["doc"]  # activated v0.4.2 (2026-06-11): minted from the v0.4.1 fleet-roll blocker. doc = KNOWN-HAZARDS 6.11 + ADR-0018 Q3 amendment. impl/unit/int activate per task (V042-PLAN.md): impl = brainproc.rs canonical-exe capture at spawn_brain_supervisor + promotion bytes-gate in supervise_brain Promoted arm (TrialEnv ready_exe_hash/staged_artifact_hash); unit = spawn-path selection (captured-default-for-None vs per-spawn) + promotion-gate truth table (match->promote, mismatch->rollback+notif, either-absent->loud PROMOTE_BYTES_UNVERIFIED degrade-to-readiness); int = the brain_survive rename-under-supervisor sibling (real in-place rename P->P.old-N + new bytes at P through the production None selection, assert respawned exe_hash == new bytes; fails Linux pre-fix)
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

# --- Added 2026-06-11 (v0.4.2): the v0.4.1 fleet-roll Linux brain-respawn blocker (ADR-0018 Q3 amendment); rule 3 register-before-satisfy, rule 5 activate per task ---
[[requirements]]
id = "REQ-HAZARD-BRAIN-RESPAWN-PATH"
title = "The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11."
required_stages = ["doc"]  # activated v0.4.2 (2026-06-11): minted from the v0.4.1 fleet-roll blocker. doc = KNOWN-HAZARDS 6.11 + ADR-0018 Q3 amendment. impl/unit/int activate per task (V042-PLAN.md): impl = brainproc.rs canonical-exe capture at spawn_brain_supervisor + promotion bytes-gate in supervise_brain Promoted arm (TrialEnv ready_exe_hash/staged_artifact_hash); unit = spawn-path selection (captured-default-for-None vs per-spawn) + promotion-gate truth table (match->promote, mismatch->rollback+notif, either-absent->loud PROMOTE_BYTES_UNVERIFIED degrade-to-readiness); int = the brain_survive rename-under-supervisor sibling (real in-place rename P->P.old-N + new bytes at P through the production None selection, assert respawned exe_hash == new bytes; fails Linux pre-fix)
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---

title = "A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists; unmintable retroactively once a migration ships."

required_stages = ["doc", "impl", "unit"]  # doc activated restoration D1 (KNOWN-HAZARDS 6.8 + ADR-0018 + design doc V1). impl/unit activated restoration D6-3 (the one D6 toml activation): impl = rollback_compat.rs (the pre-ready durable-file registry PRE_READY_DURABLE_FILES — the enumerated guard surface) + the two-phase AppliedRecord (relcache.rs, additive durable state); unit = the rollback_compat tripwire (every current pre-ready file shape is additive / N-1-readable; a non-additive pre-ready change trips the assert). The two-phase record + the generation-stamped brain.ready are additive → N-1-safe by construction (KH 6.8 D6 guard note)

+# --- Added 2026-06-11 (v0.4.2): the v0.4.1 fleet-roll Linux brain-respawn blocker (ADR-0018 Q3 amendment); rule 3 register-before-satisfy, rule 5 activate per task ---

+title = "The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11."

+required_stages = ["doc"]  # activated v0.4.2 (2026-06-11): minted from the v0.4.1 fleet-roll blocker. doc = KNOWN-HAZARDS 6.11 + ADR-0018 Q3 amendment. impl/unit/int activate per task (V042-PLAN.md): impl = brainproc.rs canonical-exe capture at spawn_brain_supervisor + promotion bytes-gate in supervise_brain Promoted arm (TrialEnv ready_exe_hash/staged_artifact_hash); unit = spawn-path selection (captured-default-for-None vs per-spawn) + promotion-gate truth table (match->promote, mismatch->rollback+notif, either-absent->loud PROMOTE_BYTES_UNVERIFIED degrade-to-readiness); int = the brain_survive rename-under-supervisor sibling (real in-place rename P->P.old-N + new bytes at P through the production None selection, assert respawned exe_hash == new bytes; fails Linux pre-fix)


T1 cont. — KNOWN-HAZARDS 6.11 entry + summary-table row. Reading insertion points.


310	- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).
311
312	---
313
314	## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
315
316	These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.
317

- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).

---

## 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.


383	| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
384	| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
385	| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |
386	| 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) |
387	| 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) |
388	| 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) |
389	| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
390	| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
391	| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |
392	| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |
393	| 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) |
394	| 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` |

| 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) |
| 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` |


- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

- **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] -->

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)


The file C:\Users\decid\Documents\projects\spt-core\docs\KNOWN-HAZARDS.md has been updated successfully. (file state is current in your context — no need to Read it back)

- **Origin:** ADR-0018 Q4 + verification amendments `[V3]`/`[V4]` (agent `doyle`); D5 plan vet (agents `todlando` + `doyle`, 2026-06-10).

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

- **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] -->

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

- **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*.

+### 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.


| 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.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.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.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.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) |

| 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) |


T1 cont. — ADR-0018 Q3 amendment note. Locating it.


Bash

ls docs/adr/ | grep -i 0018; echo "==="; grep -n "Q3\|current_exe\|executable path\|respawn" docs/adr/*0018* 2>/dev/null | head -15

Find ADR-0018 Q3 section


0018-broker-brain-process-isolation-restoration.md
===
24:2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from.


24	2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from.
25	3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`** Only phase-significant loops convert — the idempotent pump cadences need none.

2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from.
3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`** Only phase-significant loops convert — the idempotent pump cadences need none.


Edit

2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from.

2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** "the executable path" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical exe path **once at broker start** as the respawn default (Windows already had path-at-start semantics via `GetModuleFileName`), and **bytes-gate promotion** (trial promotes only if `brain.ready` `exe_hash` == the staged artifact hash, else auto-rollback). See `REQ-HAZARD-BRAIN-RESPAWN-PATH` / KNOWN-HAZARDS 6.11 / `V042-PLAN.md`.


The file C:\Users\decid\Documents\projects\spt-core\docs\adr\0018-broker-brain-process-isolation-restoration.md has been updated successfully. (file state is current in your context — no need to Read it back)

# Restore broker/brain process isolation (correct the in-process-collapse regression)

## Status

Accepted (2026-06-09) — **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.

## Context

ADR-0004 decided the broker/brain split **as two processes** — a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain — specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.

On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision was made).

Consequences of the drift:
- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**
- `spt update apply` performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — a no-op for a binary swap. `update.rs:233-234`'s "the live daemon execs the verified new binary" is aspirational and was never wired.
- New code does not run until an unrelated restart/logon. Observed live: `enlyzeam` ran 0.3.0 with the valid 0.3.2 binary on disk for ~a day, continuing to reproduce the `\r`-corruption bug the update was meant to fix.
- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.

## Decision

**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):

1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.
2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from.
3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`** Only phase-significant loops convert — the idempotent pump cadences need none.
4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update is new-brain × old-broker, so a CI-real old-broker × new-brain compat test across the verb surface is required (else KH-2.3 returns).
5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).
6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable), or auto-rollback silently breaks on the first schema migration.
7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.
8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.

## Consequences

- **Decomposition:** `spt daemon run` becomes the broker process entry (binds seed-control + broker socket + NetHost + digest hub; holds children + `EffectJournal`; spawns the brain); a new hidden `spt daemon brain` is the brain entry (connects, runs the logic loops, rehydrates from disk, emits `ready`). `ensure_running`/`is_running`/`daemon stop` contracts unchanged. The de-elevation guard applies at the broker entry.
- **Requirement registry:** REQ-DAEMON-2 and REQ-UPD-3's `int` evidence must be **re-pointed** to a productionized SPIKE-01/03 E2E proving *process-level* endpoint survival (`[V5]`). Two new hazards are minted: `REQ-HAZARD-BROKER-PROCESS-ISOLATION` (a brain restart must never drop a hosted endpoint) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`. A KNOWN-HAZARDS §6.7 entry records the regression.
- **Sequencing:** the restoration is the next milestone, before `spt-claude-code` scoping (operator-accepted 2026-06-09). Rationale: it is the *last* release that needs a manual fleet daemon bounce — paid 3× for v0.3.2 — so every adapter-era release rolls seamlessly; and the adapter is better built on the final topology than atop a daemon under later surgery while it hosts the user's daily driver. The split changes daemon internals, not the M8-frozen CLI/api surface.
- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred "future polish" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).

-2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from.

+2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** "the executable path" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical exe path **once at broker start** as the respawn default (Windows already had path-at-start semantics via `GetModuleFileName`), and **bytes-gate promotion** (trial promotes only if `brain.ready` `exe_hash` == the staged artifact hash, else auto-rollback). See `REQ-HAZARD-BRAIN-RESPAWN-PATH` / KNOWN-HAZARDS 6.11 / `V042-PLAN.md`.


T1 doc evidence complete (REQ minted at `doc`, KH 6.11 + table + `[doc->]` tag, ADR-0018 Q3 amendment). Running gates for T1 (traceable + xtask check are the load-bearing ones for doc-only).


Bash

echo "=== traceable-reqs check ==="; traceable-reqs check 2>&1 | grep -iE "BRAIN-RESPAWN|FAIL|error|missing|MISSING" | head; echo "TR-EXIT:${PIPESTATUS[0]}"; traceable-reqs check >/dev/null 2>&1; echo "TR-OVERALL:$?"; echo "=== xtask check ==="; cargo run -p xtask -- check 2>&1 | tail -4

T1 gates: traceable + xtask check


=== traceable-reqs check ===
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
  [OK] REQ-HAZARD-BRAIN-RESPAWN-PATH  required: [doc]  stages: +doc -impl -unit -int
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
TR-EXIT:0
TR-OVERALL:0
=== xtask check ===
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.87s
     Running `target\debug\xtask.exe check`
xtask check: OK


T1 gates green (traceable `[OK] required:[doc]`, xtask check OK; doc-only so build/test/clippy unaffected). Committing T1.


git add traceable-reqs.toml docs/KNOWN-HAZARDS.md docs/adr/0018-broker-brain-process-isolation-restoration.md V042-PLAN.md && git commit -q -F - <<'EOF'
docs(daemon): mint REQ-HAZARD-BRAIN-RESPAWN-PATH — Linux brain-respawn bug (KH 6.11)

The v0.4.1 fleet roll found that on Linux a brain-only `apply` respawns the
brain onto OLD bytes: spawn_brain_child resolves std::env::current_exe() per
spawn, and Linux readlink(/proc/self/exe) is inode-tracking and follows the
apply-rename (spt -> spt.old-N), so the resident broker re-execs the renamed
old binary while recording applied:N. Windows GetModuleFileName is path-at-start
so it was green (hfenduleam green, kitsubito red). ADR-0018 Q3 silently assumed
path-string semantics.

T1 (doc) of V042-PLAN.md:
- traceable-reqs.toml: mint REQ-HAZARD-BRAIN-RESPAWN-PATH (required_stages=[doc])
- KNOWN-HAZARDS 6.11 (sibling of 6.7) + summary-table row
- ADR-0018 Q3 amendment note
- V042-PLAN.md build plan (doyle-signed)

impl/unit/int activate in following commits (rule 5).

[doc->REQ-HAZARD-BRAIN-RESPAWN-PATH]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EOF
git --no-pager log -1 --oneline

Commit T1 (doc)


warning: in the working copy of 'docs/adr/0018-broker-brain-process-isolation-restoration.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'V042-PLAN.md', LF will be replaced by CRLF the next time Git touches it
8aa309e docs(daemon): mint REQ-HAZARD-BRAIN-RESPAWN-PATH — Linux brain-respawn bug (KH 6.11)


T1 committed @ `8aa309e`. **T2 (impl).** Half 1 is a localized change (capture at `spawn_brain_supervisor`); Half 2 needs the staged-artifact-hash + ready-exe-hash readers. Reading `ready_generation_at` (to mirror for exe_hash) + `ReleaseCache`/metadata/`current_platform` for the staged hash.


echo "=== ready_generation_at + brain_ready stamp ==="; grep -n "fn ready_generation_at\|fn brain_ready_path\|exe_hash\|fn ready_generation\b" crates/spt-daemon/src/brainproc.rs | head
echo "=== ReleaseCache methods ==="; grep -n "pub fn\|impl ReleaseCache\|fn applied_state\|struct ReleaseCache\|release.json\|fn staged\|metadata\|SignedRelease\|SignedUpdateSet" crates/spt-daemon/src/relcache.rs | head -40

Find ready_generation_at + ReleaseCache API


=== ready_generation_at + brain_ready stamp ===
293:/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads
298:/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this
309:    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader
311:    if let Some(hash) = current_exe_hash() {
312:        obj["exe_hash"] = serde_json::Value::String(hash);
319:/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`
323:fn current_exe_hash() -> Option<String> {
341:/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).
346:pub fn parse_ready_exe_hash(body: &str) -> Option<String> {
349:        .get("exe_hash")?
=== ReleaseCache methods ===
4://! A node holds at most **one staged release**: the newest `SignedRelease` +
15://! (`spt-store::atomic`), artifact first, metadata last — `release.json`'s
17://! metadata file whose artifact is missing or torn mid-write. A digest
30:use crate::release::{SignedRelease, SignedUpdateSet, UpdateSetMetadata};
32:/// File name of the staged release's signed metadata (the commit point).
33:const RELEASE_FILE: &str = "release.json";
40:/// [`SignedRelease`] carries no platform identity in its signed metadata
46:/// the signed metadata and never use this stamp.
111:/// envelope; the signed metadata's inner schema distinguishes them.
115:    Single(SignedRelease),
116:    Set(SignedUpdateSet),
120:    pub fn metadata_json(&self) -> &str {
122:            StagedUpdate::Single(s) => &s.metadata_json,
123:            StagedUpdate::Set(s) => &s.metadata_json,
127:    pub fn signature_hex(&self) -> &str {
134:    pub fn version(&self) -> Option<u64> {
135:        let meta: serde_json::Value = serde_json::from_str(self.metadata_json()).ok()?;
139:    pub fn channel(&self) -> Option<String> {
140:        let meta: serde_json::Value = serde_json::from_str(self.metadata_json()).ok()?;
146:    /// The human-facing semantic version stamped in the signed metadata
148:    /// absent/empty (pre-v0.3.2 metadata) — the apply message falls back to the
151:    pub fn product_version(&self) -> Option<String> {
152:        let meta: serde_json::Value = serde_json::from_str(self.metadata_json()).ok()?;
161:/// `(SignedRelease, artifact)` pair. Constructed with an explicit directory —
165:pub struct ReleaseCache {
169:impl ReleaseCache {
171:    pub fn open(dir: &Path) -> ReleaseCache {
178:    pub fn artifact_path(&self) -> PathBuf {
184:    pub fn artifact_path_for_platform(&self, platform: &str) -> PathBuf {
198:    /// Stage a verified release: artifact first, signed metadata last (the
206:    pub fn stage(&self, signed: &SignedRelease, artifact: &[u8]) -> io::Result<()> {
227:    /// metadata.
229:    pub fn stage_single_with_platform(
231:        signed: &SignedRelease,
246:    pub fn staged_platform(&self) -> Option<String> {
256:    /// metadata last (commit point). The caller MUST have verified that the
259:    pub fn stage_update_set(
261:        signed: &SignedUpdateSet,
266:        // A set self-describes its platforms in signed metadata; the single
277:    /// The staged release's signed metadata, if a complete pair is present.


`parse_ready_exe_hash` already exists (l.346). Reading the staged-metadata reader + `current_platform` + metadata artifact shape to write `staged_artifact_hash`.


277	    /// The staged release's signed metadata, if a complete pair is present.
278	    /// `None` when nothing is staged, the metadata is unreadable/corrupt, or
279	    /// the artifact file is missing (a torn stage never offers) — degrade,
280	    /// don't die.
281	    // [impl->REQ-UPD-1]
282	    pub fn staged(&self) -> Option<SignedRelease> {
283	        match self.staged_update()? {
284	            StagedUpdate::Single(s) => Some(s),
285	            StagedUpdate::Set(_) => None,
286	        }
287	    }
288
289	    /// Any complete staged update, legacy single or platform-targeted set.
290	    // [impl->REQ-UPD-6]
291	    pub fn staged_update(&self) -> Option<StagedUpdate> {
292	        let signed: SignedRelease =
293	            serde_json::from_str(&std::fs::read_to_string(self.release_path()).ok()?).ok()?;
294	        if let Ok(set_meta) = serde_json::from_str::<UpdateSetMetadata>(&signed.metadata_json) {
295	            if set_meta
296	                .artifacts
297	                .keys()
298	                .any(|p| self.artifact_path_for_platform(p).exists())
299	            {
300	                Some(StagedUpdate::Set(SignedUpdateSet {
301	                    metadata_json: signed.metadata_json,
302	                    signature_hex: signed.signature_hex,
303	                }))
304	            } else {
305	                None
306	            }
307	        } else if self.artifact_path().exists() {
308	            Some(StagedUpdate::Single(signed))
309	        } else {
310	            None
311	        }
312	    }
313
314	    /// The staged artifact bytes (for serving / re-verification).
315	    pub fn load_artifact(&self) -> io::Result<Vec<u8>> {
316	        std::fs::read(self.artifact_path())
317	    }
318
319	    /// Load the bytes for `platform` from the staged update. Legacy releases
320	    /// ignore `platform`; update sets require a matching platform file.
321	    // [impl->REQ-UPD-6]
322	    pub fn load_artifact_for_platform(&self, platform: &str) -> io::Result<Vec<u8>> {
323	        match self.staged_update() {
324	            Some(StagedUpdate::Single(_)) => self.load_artifact(),
325	            Some(StagedUpdate::Set(_)) => std::fs::read(self.artifact_path_for_platform(platform)),
326	            None => Err(io::Error::new(
327	                io::ErrorKind::NotFound,
328	                "no complete staged update",
329	            )),
330	        }
331	    }
332
333	    /// The staged release's version, `None` when nothing is staged (or the
334	    /// staged metadata doesn't parse — same degrade posture as
335	    /// [`staged`](Self::staged)). The update pump's `current_version` floor:
336	    /// only strictly-newer offers pass the monotonic gate

    /// The staged release's signed metadata, if a complete pair is present.
    /// `None` when nothing is staged, the metadata is unreadable/corrupt, or
    /// the artifact file is missing (a torn stage never offers) — degrade,
    /// don't die.
    // [impl->REQ-UPD-1]
    pub fn staged(&self) -> Option<SignedRelease> {
        match self.staged_update()? {
            StagedUpdate::Single(s) => Some(s),
            StagedUpdate::Set(_) => None,
        }
    }

    /// Any complete staged update, legacy single or platform-targeted set.
    // [impl->REQ-UPD-6]
    pub fn staged_update(&self) -> Option<StagedUpdate> {
        let signed: SignedRelease =
            serde_json::from_str(&std::fs::read_to_string(self.release_path()).ok()?).ok()?;
        if let Ok(set_meta) = serde_json::from_str::<UpdateSetMetadata>(&signed.metadata_json) {
            if set_meta
                .artifacts
                .keys()
                .any(|p| self.artifact_path_for_platform(p).exists())
            {
                Some(StagedUpdate::Set(SignedUpdateSet {
                    metadata_json: signed.metadata_json,
                    signature_hex: signed.signature_hex,
                }))
            } else {
                None
            }
        } else if self.artifact_path().exists() {
            Some(StagedUpdate::Single(signed))
        } else {
            None
        }
    }

    /// The staged artifact bytes (for serving / re-verification).
    pub fn load_artifact(&self) -> io::Result<Vec<u8>> {
        std::fs::read(self.artifact_path())
    }

    /// Load the bytes for `platform` from the staged update. Legacy releases
    /// ignore `platform`; update sets require a matching platform file.
    // [impl->REQ-UPD-6]
    pub fn load_artifact_for_platform(&self, platform: &str) -> io::Result<Vec<u8>> {
        match self.staged_update() {
            Some(StagedUpdate::Single(_)) => self.load_artifact(),
            Some(StagedUpdate::Set(_)) => std::fs::read(self.artifact_path_for_platform(platform)),
            None => Err(io::Error::new(
                io::ErrorKind::NotFound,
                "no complete staged update",
            )),
        }
    }

    /// The staged release's version, `None` when nothing is staged (or the
    /// staged metadata doesn't parse — same degrade posture as
    /// [`staged`](Self::staged)). The update pump's `current_version` floor:
    /// only strictly-newer offers pass the monotonic gate


Reading the metadata structs + `current_platform` in release.rs to build `staged_artifact_hash`.


grep -n "fn current_platform\|struct UpdateSetMetadata\|struct ReleaseMetadata\|struct.*Artifact\|artifact_sha256\|pub artifacts\|pub version" crates/spt-daemon/src/release.rs | head -30

Find metadata structs + current_platform


21://! its SHA-256 ([`ReleaseMetadata::artifact_sha256`]) — so [`verify_artifact`]
44:pub struct ReleaseMetadata {
47:    pub version: u64,
56:    pub artifact_sha256: String,
96:pub struct UpdateArtifactMetadata {
98:    pub artifact_sha256: String,
137:pub struct UpdateSetMetadata {
139:    pub version: u64,
147:    pub artifacts: BTreeMap<String, UpdateArtifactMetadata>,
186:pub fn current_platform() -> &'static str {
355:    /// The actual binary's digest does not match the signed `artifact_sha256`.
537:/// Confirm the downloaded binary's bytes match the signed `artifact_sha256`
543:    if actual == meta.artifact_sha256.to_ascii_lowercase() {
547:            signed: meta.artifact_sha256.clone(),
563:    if actual == entry.artifact_sha256.to_ascii_lowercase() {
567:            signed: entry.artifact_sha256.clone(),
574:/// [`ReleaseMetadata::artifact_sha256`].
654:            artifact_sha256: sha256_hex(artifact),
671:                        artifact_sha256: sha256_hex(win),
680:                        artifact_sha256: sha256_hex(linux),


All metadata pieces located (`ReleaseMetadata`, `UpdateSetMetadata.artifacts`, `current_platform`). Adding `staged_artifact_hash` to ReleaseCache first.


333	    /// The staged release's version, `None` when nothing is staged (or the
334	    /// staged metadata doesn't parse — same degrade posture as
335	    /// [`staged`](Self::staged)). The update pump's `current_version` floor:
336	    /// only strictly-newer offers pass the monotonic gate
337	    /// (REQ-HAZARD-UPDATE-ROLLBACK).
338	    // [impl->REQ-UPD-1]
339	    pub fn staged_version(&self) -> Option<u64> {
340	        self.staged_update()?.version()
341	    }
342
343	    /// The staged update's channel, if present.
344	    // [impl->REQ-UPD-6]
345	    pub fn staged_channel(&self) -> Option<String> {
346	        self.staged_update()?.channel()
347	    }
348
349	    fn applied_path(&self) -> PathBuf {
350	        self.dir.join("applied.json")
351	    }
352

    /// The staged release's version, `None` when nothing is staged (or the
    /// staged metadata doesn't parse — same degrade posture as
    /// [`staged`](Self::staged)). The update pump's `current_version` floor:
    /// only strictly-newer offers pass the monotonic gate
    /// (REQ-HAZARD-UPDATE-ROLLBACK).
    // [impl->REQ-UPD-1]
    pub fn staged_version(&self) -> Option<u64> {
        self.staged_update()?.version()
    }

    /// The staged update's channel, if present.
    // [impl->REQ-UPD-6]
    pub fn staged_channel(&self) -> Option<String> {
        self.staged_update()?.channel()
    }

    fn applied_path(&self) -> PathBuf {
        self.dir.join("applied.json")
    }


Edit

    /// The staged update's channel, if present.
    // [impl->REQ-UPD-6]
    pub fn staged_channel(&self) -> Option<String> {
        self.staged_update()?.channel()
    }

    /// The staged update's channel, if present.
    // [impl->REQ-UPD-6]
    pub fn staged_channel(&self) -> Option<String> {
        self.staged_update()?.channel()
    }

    /// The staged update's expected artifact SHA-256 for THIS platform, gated
    /// on `version` matching the staged metadata — the bytes the promotion gate
    /// (KH 6.11, REQ-HAZARD-BRAIN-RESPAWN-PATH) compares a just-promoted
    /// candidate's `brain.ready` `exe_hash` against. `None` when nothing is
    /// staged, the staged version != `version`, the metadata doesn't parse, or
    /// this platform is absent from a set — every `None` degrades the gate to
    /// readiness-only (N-1-safe), never a false rejection.
    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]
    pub fn staged_artifact_hash(&self, version: u64) -> Option<String> {
        match self.staged_update()? {
            StagedUpdate::Single(s) => {
                let meta: ReleaseMetadata = serde_json::from_str(&s.metadata_json).ok()?;
                (meta.version == version).then(|| meta.artifact_sha256.to_ascii_lowercase())
            }
            StagedUpdate::Set(s) => {
                let meta: UpdateSetMetadata = serde_json::from_str(&s.metadata_json).ok()?;
                (meta.version == version)
                    .then(|| meta.artifacts.get(current_platform()))
                    .flatten()
                    .map(|a| a.artifact_sha256.to_ascii_lowercase())
            }
        }
    }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\relcache.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// The staged update's channel, if present.
    // [impl->REQ-UPD-6]
    pub fn staged_channel(&self) -> Option<String> {
        self.staged_update()?.channel()
    }

    /// The staged update's expected artifact SHA-256 for THIS platform, gated
    /// on `version` matching the staged metadata — the bytes the promotion gate
    /// (KH 6.11, REQ-HAZARD-BRAIN-RESPAWN-PATH) compares a just-promoted
    /// candidate's `brain.ready` `exe_hash` against. `None` when nothing is
    /// staged, the staged version != `version`, the metadata doesn't parse, or
    /// this platform is absent from a set — every `None` degrades the gate to
    /// readiness-only (N-1-safe), never a false rejection.
    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]
    pub fn staged_artifact_hash(&self, version: u64) -> Option<String> {
        match self.staged_update()? {
            StagedUpdate::Single(s) => {
                let meta: ReleaseMetadata = serde_json::from_str(&s.metadata_json).ok()?;
                (meta.version == version).then(|| meta.artifact_sha256.to_ascii_lowercase())
            }
            StagedUpdate::Set(s) => {
                let meta: UpdateSetMetadata = serde_json::from_str(&s.metadata_json).ok()?;
                (meta.version == version)
                    .then(|| meta.artifacts.get(current_platform()))
                    .flatten()
                    .map(|a| a.artifact_sha256.to_ascii_lowercase())
            }
        }
    }

+    /// The staged update's expected artifact SHA-256 for THIS platform, gated

+    /// on `version` matching the staged metadata — the bytes the promotion gate

+    /// (KH 6.11, REQ-HAZARD-BRAIN-RESPAWN-PATH) compares a just-promoted

+    /// candidate's `brain.ready` `exe_hash` against. `None` when nothing is

+    /// staged, the staged version != `version`, the metadata doesn't parse, or

+    /// this platform is absent from a set — every `None` degrades the gate to

+    /// readiness-only (N-1-safe), never a false rejection.

+    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]

+    pub fn staged_artifact_hash(&self, version: u64) -> Option<String> {

+            StagedUpdate::Single(s) => {

+                let meta: ReleaseMetadata = serde_json::from_str(&s.metadata_json).ok()?;

+                (meta.version == version).then(|| meta.artifact_sha256.to_ascii_lowercase())

+                let meta: UpdateSetMetadata = serde_json::from_str(&s.metadata_json).ok()?;

+                (meta.version == version)

+                    .then(|| meta.artifacts.get(current_platform()))

+                    .map(|a| a.artifact_sha256.to_ascii_lowercase())


Adding the imports in relcache.rs.


use crate::release::{SignedRelease, SignedUpdateSet, UpdateSetMetadata};

use crate::release::{
    current_platform, ReleaseMetadata, SignedRelease, SignedUpdateSet, UpdateSetMetadata,
};


use crate::release::{
    current_platform, ReleaseMetadata, SignedRelease, SignedUpdateSet, UpdateSetMetadata,
};

-use crate::release::{SignedRelease, SignedUpdateSet, UpdateSetMetadata};

+    current_platform, ReleaseMetadata, SignedRelease, SignedUpdateSet, UpdateSetMetadata,

/// File name of the staged release's signed metadata (the commit point).

const RELEASE_FILE: &str = "release.json";


Now brainproc.rs. Adding the two methods to the `TrialEnv` trait.


    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);
}

    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);
    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is
    /// actually running), or `None` if absent/garbled — degrades the promotion
    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).
    fn ready_exe_hash(&self) -> Option<String>;
    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes
    /// the candidate SHOULD be running), or `None` if no staged metadata /
    /// version mismatch / platform absent — degrades the gate to readiness-only.
    fn staged_artifact_hash(&self, version: u64) -> Option<String>;
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\brainproc.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);
}

    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);
    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is
    /// actually running), or `None` if absent/garbled — degrades the promotion
    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).
    fn ready_exe_hash(&self) -> Option<String>;
    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes
    /// the candidate SHOULD be running), or `None` if no staged metadata /
    /// version mismatch / platform absent — degrades the gate to readiness-only.
    fn staged_artifact_hash(&self, version: u64) -> Option<String>;
}

/// Correct the record: the candidate failed readiness. Persist

/// `RolledBack{…}` and fire the loud, resurfacing rollback notif.

+    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is

+    /// actually running), or `None` if absent/garbled — degrades the promotion

+    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).

+    fn ready_exe_hash(&self) -> Option<String>;

+    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes

+    /// the candidate SHOULD be running), or `None` if no staged metadata /

+    /// version mismatch / platform absent — degrades the gate to readiness-only.

+    fn staged_artifact_hash(&self, version: u64) -> Option<String>;

/// The no-trial environment: every probe degrades to "nothing pending / not


        let _ = cache.record_last_outcome(&format!("rolled-back:{quarantine_version}"));
        fire_rollback_notif(quarantine_version, running_version);
    }
}

        let _ = cache.record_last_outcome(&format!("rolled-back:{quarantine_version}"));
        fire_rollback_notif(quarantine_version, running_version);
    }
    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]
    fn ready_exe_hash(&self) -> Option<String> {
        let body = std::fs::read_to_string(brain_ready_path()).ok()?;
        parse_ready_exe_hash(&body)
    }
    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]
    fn staged_artifact_hash(&self, version: u64) -> Option<String> {
        self.cache().staged_artifact_hash(version)
    }
}


        let _ = cache.record_last_outcome(&format!("rolled-back:{quarantine_version}"));
        fire_rollback_notif(quarantine_version, running_version);
    }
}

        let _ = cache.record_last_outcome(&format!("rolled-back:{quarantine_version}"));
        fire_rollback_notif(quarantine_version, running_version);
    }
    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]
    fn ready_exe_hash(&self) -> Option<String> {
        let body = std::fs::read_to_string(brain_ready_path()).ok()?;
        parse_ready_exe_hash(&body)
    }
    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]
    fn staged_artifact_hash(&self, version: u64) -> Option<String> {
        self.cache().staged_artifact_hash(version)
    }
}

+    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]

+    fn ready_exe_hash(&self) -> Option<String> {

+        let body = std::fs::read_to_string(brain_ready_path()).ok()?;

+    fn staged_artifact_hash(&self, version: u64) -> Option<String> {

+        self.cache().staged_artifact_hash(version)

/// Fire the loud, resurfacing auto-rollback notif on every member subnet


Adding the `BytesGate` enum + `bytes_gate` helper (after `rollback`).