1	# Broker/Brain Process-Isolation Restoration — Milestone Build Plan
2
3	> **Status: RATIFIED (2026-06-09).** Design (`docs/BROKER-BRAIN-SPLIT-RESTORATION.md`,
4	> verified-with-amendments by `doyle`) and decision (`docs/adr/0018-...`) are
5	> operator-ratified; sequencing ratified as the **next** milestone, before
6	> `spt-claude-code`. REQs minted inactive (TRACEABILITY rule 3): this plan is
7	> the build, and schedules their activation per task (rule 5: activate at the
8	> commit that delivers the evidence). This is the **last** release that needs
9	> a manual fleet daemon bounce.
10
11	Goal: restore ADR-0004's intended **two-process** model so a routine
12	brain-only update hands off to the new executable seamlessly — no endpoint
13	or harness child dropped (REQ-UPD-3), loop timings continuous across the
14	swap, failures fail *safe*, uniform on Windows + Linux. The production daemon
15	today runs the broker as a background **thread** inside the single `spt
16	daemon` process (`daemon.rs:165-170`), so `apply` swaps the binary on disk
17	but never restarts the running code — the no-endpoint-drop self-update pillar
18	is silently unrealized. Restore it as a real process boundary.
19
20	This changes daemon **internals only** — the M8-frozen CLI/api surface is
21	untouched, so the adapter window stays open and `spt-claude-code` lands on
22	the final topology.
23
24	## Why now (sequencing — ratified)
25
26	1. **Queue open** — mesh shipped v0.3.0, M8 acceptance COMPLETE.
27	2. **Self-bootstrapping (decisive)** — until this ships, *every* release needs
28	   a manual fleet-wide daemon bounce (paid 3× on v0.3.2; `enlyzeam` ran 0.3.0
29	   with the 0.3.2 fix on disk for ~a day, still reproducing the `\r`-corruption).
30	   The restoration release is the last needing a manual bounce.
31	3. **Adapter window safe** — internals only; better the adapter lands on the
32	   final topology than atop a daemon under later open-heart surgery while it
33	   hosts the user's daily driver.
34
35	## What "done" looks like (goal-backward)
36
37	- `spt daemon run` is the **broker** process: binds seed-control + broker
38	  socket + NetHost + digest hub; holds PTY masters / harness children /
39	  `EffectJournal`; spawns the **brain** as a child and supervises it.
40	- A hidden `spt daemon brain` entry is the brain: connects to the broker over
41	  socket IPC, runs the logic loops, rehydrates from disk, emits `ready`.
42	- `spt update apply` swaps the binary, signals the brain to snapshot +
43	  self-exit, the broker respawns from the executable path (now the new
44	  binary), the new brain re-attaches **all** sessions in resume mode — and a
45	  live PTY child's pid is unchanged and its QUIC conn survives, at the
46	  **process** level. New code runs immediately, no logon/manual bounce.
47	- A new brain that fails to reach `ready` within the bounded window
48	  auto-rolls-back to `spt.exe.old-N`, quarantines the bad version, and fires a
49	  loud notif — endpoints alive throughout.
50	- `ensure_running` / `is_running` / `daemon stop` contracts unchanged (target
51	  the broker's seed channel); de-elevation guard applies at the broker entry.
52
53	---
54
55	## Task decomposition (dependency-ordered)
56
57	Each task maps to ADR-0018 decisions (Q2–Q8, amendments V1–V6) and lands as
58	its own atomic commit(s) with evidence tagged in-commit. Build order is the
59	dependency order: the process split is foundational; rollback + the proof
60	come last.
61
62	### D1 — Process split skeleton (foundation) · Q8, §5
63
64	The structural cut, proven minimal before any logic moves.
65
66	- Add the hidden **`spt daemon brain`** subcommand (brain process entry).
67	  `spt daemon run` stays the user-facing entry but becomes the **broker**
68	  process: it binds the seed-control channel + broker socket + NetHost +
69	  digest hub (as today), then **`Command::spawn`s the brain child** instead
70	  of `thread::spawn`ing the logic loops in-process.
71	- Broker ↔ brain talk over the **existing local socket IPC** (the versioned
72	  contract already carrying `SPAWN`/`INPUT`/`NET_*`/`SESSIONS`). No `exec`,
73	  no FD-passing → no Windows/Unix divergence (Q8).
74	- Brain connects on boot, completes the IPC handshake, runs the logic loops
75	  (pump/dispatch/psyche/session drivers) as today but **in its own process**,
76	  and emits a **`ready`** signal once attached. Broker `connect_retry`
77	  (`applyhost.rs`) is the brain-side connect primitive, reused.
78	- De-elevation guard (`daemon.rs:60-103`) stays at the **broker** entry; the
79	  brain child inherits the unelevated identity.
80	- **Acceptance:** `spt daemon run` brings up a broker process + a distinct
81	  brain child pid; killing the brain leaves the broker (and any PTY child)
82	  alive. No behavior change visible at the CLI.
83
84	### D2 — IPC verb surface + N-1 compat scaffolding · Q5, V6
85
86	With two processes there is no shared `Arc<Broker>`; every brain→broker
87	direct call becomes a versioned IPC verb on the same contract.
88
89	- **Audit** every current in-process `Arc<Broker>` consumer; convert each
90	  direct call to a versioned verb. Per §Q5:
91	  - **Net bring-up + REQ-DAEMON-9 self-heal retry → broker** — *near-free*:
92	    NetHost is already broker-owned (`broker.rs:175` `OnceLock`), and net
93	    bring-up already lives in the entry that *becomes* the broker entry.
94	  - **Digest hub → broker** (live projections + subscriber sockets); **the
95	    parse that feeds it → brain** (brain pushes parsed digests over IPC).
96	  - **Seed-lock + liveness → broker** (the Q2 anchor; see D3).
97	  - Every other brain→broker call → a versioned verb, defaulted/additive per
98	    KH-2.3 (handoff-argv-compat).
99	- **[V6] N-1 compat is milestone work.** Steady state after every routine
100	  update is **new-brain × old-broker** (broker almost never updates). The
101	  `classify` pre-swap handshake (`brain_ipc_version ≥ broker.min_compatible`)
102	  gates the forward direction, but each verb conversion must hold the N-1
103	  window. Build the **CI-real compat test** here: an **old-broker binary ×
104	  new-brain binary** exercise across the whole verb surface (else KH-2.3
105	  returns the first time a verb signature changes). Scaffold it in D2; it
106	  becomes a gate in D7.
107	- **Exception (Q5):** shellwake `wake_command` watcher **children stay
108	  brain-owned**, re-reconciled from disk on brain start (the boot sweep at
109	  `daemon.rs:206-217` already does orphan cleanup). Accepted: a rare brain
110	  update may briefly miss a wake for an offline shell.
111
112	### D3 — Supervision anchor + update trigger · Q2, Q3, V2
113
114	The broker becomes the always-up per-machine anchor and the brain's supervisor.
115
116	- **Seed-control lock + liveness move to the broker** (Q2): the
117	  single-daemon-per-`SPT_HOME` invariant and the `ensure_running`/`is_running`
118	  ping target live in the never-restarting layer, so a brain restart can't let
119	  a second daemon win the bind or make `is_running` flap. `ensure_running` /
120	  `is_running` / `daemon stop` external contracts are **unchanged** — they
121	  target the broker's seed channel (which is where they already point).
122	- **Broker supervises + respawns the brain** as its child (the crash-recovery
123	  path it must already provide).
124	- **Update trigger (Q3):** `apply` verifies + swaps the binary on disk, then
125	  signals the brain to **snapshot + self-exit**. The broker observes the exit
126	  and **respawns from the executable path (now the new binary)**; the new
127	  brain re-attaches. This reuses the existing `apply_brain_only` snapshot→drop→
128	  re-attach primitive — an update is a *planned* crash on the path the broker
129	  already recovers from. (`update.rs:233-234`'s "exec the new binary's brain"
130	  finally becomes real, via respawn rather than exec.)
131	- **[V2] Generation custody → broker** (Q2): retiring the `BrainState`
132	  message (D4) would orphan the KH-2.4 generation counter. The broker owns it,
133	  increments on **every** brain spawn (planned or crash), and hands
134	  `{generation, start-reason}` to the brain at spawn time via a **versioned
135	  argv/hello field** (KH-2.3 forward-compat, defaulted). Because the broker
136	  observes every respawn this is strictly more reliable than brain→brain.
137	- The **same spawn-time channel carries Q4's update-vs-crash discriminator**
138	  (one channel, both payloads): the broker *initiated* a planned cycle but
139	  *observed* an unexpected crash.
140
141	### D4 — Multi-session resume; retire the `BrainState` message · Q6
142
143	Under the self-exit trigger the outgoing brain is gone before the new one
144	starts, so continuity must come from the persistent side, not a brain→brain
145	frame.
146
147	- The **broker becomes cursor-of-record** per session-subscriber (it already
148	  holds the per-session output ring, `broker.rs:73-143`): track each
149	  subscriber's last-**delivered** cursor.
150	- On (re)start the new brain queries the broker for **all** hosted sessions
151	  and re-attaches each in **resume** mode — fixing today's two gaps:
152	  `applyhost.rs:239` re-attaches only `sessions.first()` (one session) with
153	  `from_seq=0` (re-replays the whole ring → duplicate output).
154	- **Output is at-least-once** (broker resumes from last-*sent*); **input/
155	  effects stay exactly-once** via the already-broker-owned `EffectJournal`.
156	  Matches the SPIKE-05 terminal-stream contract (a resize repaint already
157	  reorders/duplicates the stream; exactly-once output would need a per-chunk
158	  ack for a guarantee the stream doesn't make).
159	- The explicit **`BrainState` handoff *message* retires** for production: the
160	  new brain cold-starts and reconstructs continuity by querying the broker.
161	  (`brain.rs` `BrainState`/`handoff` stay available for tests; the production
162	  path stops passing the frame.)
163
164	### D5 — Durable absolute-deadline loop timing · Q4, V3, V4
165
166	Timing that must survive a swap lives as durable absolute-deadline state on
167	disk, rehydrated on every brain start — not in the (planned-only) handoff
168	snapshot, which a crash carries nothing of.
169
170	- **Phase-significant resident loops only [V4].** The **pulse loop**
171	  (`lifecycle.rs:486` `sleep_interruptible(pulse_period)`, phase-relative →
172	  resets on restart) converts to a disk-anchored deadline. The **idempotent
173	  pump cadences need NO conversion** — they already "stagger from everything
174	  due now" (`peerloop.rs:304`), catch-up/idempotent, restart-safe; converting
175	  them would reintroduce the per-loop writes Q4 minimized.
176	- **Mechanism:** persist `(anchor, interval)` **once** on a fresh/crash start;
177	  derive `next_fire = anchor + interval × ⌈max(0, now − anchor) / interval⌉` —
178	  **no per-fire writes**. An **update restart re-reads `(anchor, interval)`
179	  and keeps deriving** (phase preserved, lands mid-grid). A **crash restart is
180	  a fresh start** (rewrite `anchor = now`; phase reset acceptable). The brain
181	  distinguishes update-vs-crash via the D3 spawn-time discriminator.
182	- **One-shot deadlines (alarms): rule fixed here, machinery deferred [V3].**
183	  Persist the absolute `target-time` at creation; every start (update *or*
184	  crash) reads it and fires-if-due; **never reset on crash** ("remind me at
185	  3pm" is a commitment). But the daemon has **no one-shot consumer today**
186	  (alarms are legacy-listener-only, §7) — building it now ships untested dead
187	  code. The *rule* is specified this milestone; the *machinery* is built with
188	  the alarm port (activate-don't-pre-fail). Tracked in DEFERRED.
189
190	### D6 — Failure atomicity: readiness-gated auto-rollback · Q7, V1
191
192	A new brain that bricks *logic* (panics on boot) must self-heal without a
193	human — endpoints stay up (the broker holds them).
194
195	- **Bounded-retry → auto-rollback.** If the new brain fails to reach `ready`
196	  (re-attached all sessions + resumed loops) within a bounded number of boots
197	  / a healthy-run window, the broker **rolls back to the last-known-good
198	  binary** (`spt.exe.old-N`), **quarantines** the bad version (no auto
199	  re-apply/re-fetch), and surfaces a loud consent-style notification. The
200	  broker holds **both** last-known-good + candidate paths and chooses which
201	  to spawn (no file manipulation at failure time). **Reuse** the existing
202	  readiness+backoff machinery: `peerloop.rs:82-91`
203	  `SUPERVISE_BACKOFF_BASE/CAP` + `SUPERVISE_HEALTHY_RUN` (60 s healthy resets
204	  backoff), `supervise_pump` wired at `peerloop.rs:805` (M8-D4).
205	- **Two-phase applied record** (fixes a real existing bug): `applyhost.rs:176`
206	  writes `record_applied(version)` + `last-outcome=applied` **before** the
207	  handoff — the optimistic `applied.json={version:6}` observed on `enlyzeam`.
208	  Make it: `applied-pending` at swap → promoted to `applied` on the `ready`
209	  signal → corrected to `rolled-back(quarantine=N, running=N-1)` on failure.
210	- **[V1] Rollback-state-compat invariant.** Auto-rollback spawns the **old**
211	  binary against durable state the **new** brain may already have written.
212	  Safe today (2026-06-09 audit: zero state-migration code), but the first
213	  durable-state schema migration would silently break rollback. Assert the
214	  invariant **now, while free**: a brain must not irreversibly migrate durable
215	  state before ready-promotion — every pre-ready write stays N-1-readable.
216	  This task lands the invariant as an explicit, tested guard/marker (and a
217	  doc note in KNOWN-HAZARDS 6.8) so a future migration trips a known wire.
218
219	### D7 — Proof + REQ re-pointing + fleet verify · V5, V6, §9
220
221	The milestone's conformance close-out.
222
223	- **Productionize SPIKE-01/03 as an `int` E2E [V5]:** a PTY child + a live
224	  QUIC conn survive a brain-**process** restart **onto a swapped binary** —
225	  pid unchanged, output stream gapless, conn intact. This is the test that
226	  proves *process-level* survival (the spikes proved the shape with two
227	  binaries; this proves it in production topology).
228	- **Re-point the regression-masked evidence.** REQ-DAEMON-2 and REQ-UPD-3's
229	  current `int` tags prove only the *in-process* handoff shape
230	  (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`, the M3b-B9 E2E) —
231	  mis-evidenced today. Move them to the D7 process-level E2E.
232	- **N-1 compat test as a CI gate [V6]:** the old-broker × new-brain
233	  verb-surface exercise scaffolded in D2 becomes a green gate.
234	- **Activate the minted hazards:** set real `required_stages` on
235	  `REQ-HAZARD-BROKER-PROCESS-ISOLATION` (doc=ADR-0018+design; impl/unit across
236	  D1–D6; int=the D7 E2E + N-1 test) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`
237	  (doc=ADR-0018+KH 6.8; impl/unit=D6 invariant guard).
238	- **Fleet verify** the real seamless update on the 3-node rig
239	  (kitsubito/hfenduleam/enlyzeam): publish a restoration+1 test release →
240	  `apply` on each node → **the running pid changes / new code goes live with
241	  no manual bounce**, every hosted endpoint survives. This is the acceptance
242	  the v0.3.2 roll could not get for free.
243
244	---
245
246	## Traceability — REQ activation schedule
247
248	| REQ | Today | This milestone | Activated at |
249	|-----|-------|----------------|--------------|
250	| `REQ-HAZARD-BROKER-PROCESS-ISOLATION` | minted, `required_stages=[]` | doc → impl → unit → int | doc now (ADR-0018+design exist); impl/unit across D1–D6; **int at D7** (process-level E2E + N-1 test) |
251	| `REQ-HAZARD-ROLLBACK-STATE-COMPAT` | minted, `required_stages=[]` | doc → impl → unit | doc now (ADR-0018+KH 6.8); **impl/unit at D6** (invariant guard) |
252	| `REQ-UPD-3` (no endpoint drop, brain-only) | `int` proves in-process shape (mis-evidenced) | re-point `int` to process-level survival | **D7** |
253	| `REQ-DAEMON-2` (broker/brain split for seamless update) | `int` proves in-process shape | re-point `int` to process-level survival | **D7** |
254
255	Rule 5 (activate-don't-pre-fail): toml `required_stages` are set at the
256	commit that delivers each stage's evidence, **not** now — `traceable-reqs
257	check` stays green at every commit. Re-pointing REQ-DAEMON-2/UPD-3 happens in
258	the same D7 commit that lands the replacement E2E (never leave them evidenced
259	by the old in-process test once the new test exists).
260
261	## Per-task gates (every commit)
262
263	`cargo build` · `cargo test` · `cargo clippy` · `cargo build
264	--no-default-features` · `traceable-reqs check` (EXIT=0) · `xtask check` (doc
265	drift). Push to a dev-freeform branch → **CI both runners** before any
266	release tag (the v0.3.0 lesson: CI catches regressions a local box can't —
267	single-home non-elevated dev box is blind to service-detection / socket-path
268	issues). The `[twohost]` rig rungs gate D7.
269
270	## Risks / watch-items
271
272	- **The hardest invariant** (de-risk spike #1): a PTY child + open socket
273	  survive a brain-process restart. The spikes proved it; D1 must re-prove it
274	  in production topology before logic moves (D3+). If D1's acceptance fails,
275	  stop — the whole design rests on it.
276	- **N-1 verb drift (KH-2.3).** Every verb added in D2 must default/additive;
277	  the D7 N-1 gate is the backstop. A breaking verb signature change is a
278	  broker-breaking update class — out of scope, must be refused, not silently
279	  shipped.
280	- **Test broker socket-bind flake** (DEFERRED, 2026-06-09): the AF_UNIX
281	  `served_broker` bind flakes on kitsubito. D1/D7 add real process-spawn
282	  tests on that runner — fix the bind determinism (unlink-before-bind /
283	  abstract namespace / per-test TempDir) opportunistically here rather than
284	  fighting flakes through the milestone.
285	- **Generation counter (KH-2.4):** custody moves to the broker (V2). Verify no
286	  consumer reads `gen_start_ms` as anything but observability (it already
287	  must not — `brain.rs:9-15`).
288
289	## Immediate next-session start
290
291	1. **D1 first** — add the `spt daemon brain` hidden entry + flip `spt daemon
292	   run` from `thread::spawn(broker.serve)` to broker-process + spawned brain
293	   child over the existing socket IPC. Prove the skeleton (broker survives
294	   brain kill, PTY child intact) **before** moving any logic.
295	2. Activate `REQ-HAZARD-BROKER-PROCESS-ISOLATION` doc stage (evidence already
296	   exists: ADR-0018 + design doc) in the D1 commit; impl/unit follow as D1
297	   lands real structure.
298	3. Keep `ensure_running`/`is_running`/`daemon stop` external behavior
299	   identical throughout — they're the single-instance contract the whole
300	   fleet depends on; only their *owner process* moves (thread → broker proc).
301	4. Do **not** convert the idempotent pump cadences (D5 [V4]) — only the pulse
302	   loop's phase-significant timing.
303

# Broker/Brain Process-Isolation Restoration — Milestone Build Plan

> **Status: RATIFIED (2026-06-09).** Design (`docs/BROKER-BRAIN-SPLIT-RESTORATION.md`,
> verified-with-amendments by `doyle`) and decision (`docs/adr/0018-...`) are
> operator-ratified; sequencing ratified as the **next** milestone, before
> `spt-claude-code`. REQs minted inactive (TRACEABILITY rule 3): this plan is
> the build, and schedules their activation per task (rule 5: activate at the
> commit that delivers the evidence). This is the **last** release that needs
> a manual fleet daemon bounce.

Goal: restore ADR-0004's intended **two-process** model so a routine
brain-only update hands off to the new executable seamlessly — no endpoint
or harness child dropped (REQ-UPD-3), loop timings continuous across the
swap, failures fail *safe*, uniform on Windows + Linux. The production daemon
today runs the broker as a background **thread** inside the single `spt
daemon` process (`daemon.rs:165-170`), so `apply` swaps the binary on disk
but never restarts the running code — the no-endpoint-drop self-update pillar
is silently unrealized. Restore it as a real process boundary.

This changes daemon **internals only** — the M8-frozen CLI/api surface is
untouched, so the adapter window stays open and `spt-claude-code` lands on
the final topology.

## Why now (sequencing — ratified)

1. **Queue open** — mesh shipped v0.3.0, M8 acceptance COMPLETE.
2. **Self-bootstrapping (decisive)** — until this ships, *every* release needs
   a manual fleet-wide daemon bounce (paid 3× on v0.3.2; `enlyzeam` ran 0.3.0
   with the 0.3.2 fix on disk for ~a day, still reproducing the `\r`-corruption).
   The restoration release is the last needing a manual bounce.
3. **Adapter window safe** — internals only; better the adapter lands on the
   final topology than atop a daemon under later open-heart surgery while it
   hosts the user's daily driver.

## What "done" looks like (goal-backward)

- `spt daemon run` is the **broker** process: binds seed-control + broker
  socket + NetHost + digest hub; holds PTY masters / harness children /
  `EffectJournal`; spawns the **brain** as a child and supervises it.
- A hidden `spt daemon brain` entry is the brain: connects to the broker over
  socket IPC, runs the logic loops, rehydrates from disk, emits `ready`.
- `spt update apply` swaps the binary, signals the brain to snapshot +
  self-exit, the broker respawns from the executable path (now the new
  binary), the new brain re-attaches **all** sessions in resume mode — and a
  live PTY child's pid is unchanged and its QUIC conn survives, at the
  **process** level. New code runs immediately, no logon/manual bounce.
- A new brain that fails to reach `ready` within the bounded window
  auto-rolls-back to `spt.exe.old-N`, quarantines the bad version, and fires a
  loud notif — endpoints alive throughout.
- `ensure_running` / `is_running` / `daemon stop` contracts unchanged (target
  the broker's seed channel); de-elevation guard applies at the broker entry.

---

## Task decomposition (dependency-ordered)

Each task maps to ADR-0018 decisions (Q2–Q8, amendments V1–V6) and lands as
its own atomic commit(s) with evidence tagged in-commit. Build order is the
dependency order: the process split is foundational; rollback + the proof
come last.

### D1 — Process split skeleton (foundation) · Q8, §5

The structural cut, proven minimal before any logic moves.

- Add the hidden **`spt daemon brain`** subcommand (brain process entry).
  `spt daemon run` stays the user-facing entry but becomes the **broker**
  process: it binds the seed-control channel + broker socket + NetHost +
  digest hub (as today), then **`Command::spawn`s the brain child** instead
  of `thread::spawn`ing the logic loops in-process.
- Broker ↔ brain talk over the **existing local socket IPC** (the versioned
  contract already carrying `SPAWN`/`INPUT`/`NET_*`/`SESSIONS`). No `exec`,
  no FD-passing → no Windows/Unix divergence (Q8).
- Brain connects on boot, completes the IPC handshake, runs the logic loops
  (pump/dispatch/psyche/session drivers) as today but **in its own process**,
  and emits a **`ready`** signal once attached. Broker `connect_retry`
  (`applyhost.rs`) is the brain-side connect primitive, reused.
- De-elevation guard (`daemon.rs:60-103`) stays at the **broker** entry; the
  brain child inherits the unelevated identity.
- **Acceptance:** `spt daemon run` brings up a broker process + a distinct
  brain child pid; killing the brain leaves the broker (and any PTY child)
  alive. No behavior change visible at the CLI.

### D2 — IPC verb surface + N-1 compat scaffolding · Q5, V6

With two processes there is no shared `Arc<Broker>`; every brain→broker
direct call becomes a versioned IPC verb on the same contract.

- **Audit** every current in-process `Arc<Broker>` consumer; convert each
  direct call to a versioned verb. Per §Q5:
  - **Net bring-up + REQ-DAEMON-9 self-heal retry → broker** — *near-free*:
    NetHost is already broker-owned (`broker.rs:175` `OnceLock`), and net
    bring-up already lives in the entry that *becomes* the broker entry.
  - **Digest hub → broker** (live projections + subscriber sockets); **the
    parse that feeds it → brain** (brain pushes parsed digests over IPC).
  - **Seed-lock + liveness → broker** (the Q2 anchor; see D3).
  - Every other brain→broker call → a versioned verb, defaulted/additive per
    KH-2.3 (handoff-argv-compat).
- **[V6] N-1 compat is milestone work.** Steady state after every routine
  update is **new-brain × old-broker** (broker almost never updates). The
  `classify` pre-swap handshake (`brain_ipc_version ≥ broker.min_compatible`)
  gates the forward direction, but each verb conversion must hold the N-1
  window. Build the **CI-real compat test** here: an **old-broker binary ×
  new-brain binary** exercise across the whole verb surface (else KH-2.3
  returns the first time a verb signature changes). Scaffold it in D2; it
  becomes a gate in D7.
- **Exception (Q5):** shellwake `wake_command` watcher **children stay
  brain-owned**, re-reconciled from disk on brain start (the boot sweep at
  `daemon.rs:206-217` already does orphan cleanup). Accepted: a rare brain
  update may briefly miss a wake for an offline shell.

### D3 — Supervision anchor + update trigger · Q2, Q3, V2

The broker becomes the always-up per-machine anchor and the brain's supervisor.

- **Seed-control lock + liveness move to the broker** (Q2): the
  single-daemon-per-`SPT_HOME` invariant and the `ensure_running`/`is_running`
  ping target live in the never-restarting layer, so a brain restart can't let
  a second daemon win the bind or make `is_running` flap. `ensure_running` /
  `is_running` / `daemon stop` external contracts are **unchanged** — they
  target the broker's seed channel (which is where they already point).
- **Broker supervises + respawns the brain** as its child (the crash-recovery
  path it must already provide).
- **Update trigger (Q3):** `apply` verifies + swaps the binary on disk, then
  signals the brain to **snapshot + self-exit**. The broker observes the exit
  and **respawns from the executable path (now the new binary)**; the new
  brain re-attaches. This reuses the existing `apply_brain_only` snapshot→drop→
  re-attach primitive — an update is a *planned* crash on the path the broker
  already recovers from. (`update.rs:233-234`'s "exec the new binary's brain"
  finally becomes real, via respawn rather than exec.)
- **[V2] Generation custody → broker** (Q2): retiring the `BrainState`
  message (D4) would orphan the KH-2.4 generation counter. The broker owns it,
  increments on **every** brain spawn (planned or crash), and hands
  `{generation, start-reason}` to the brain at spawn time via a **versioned
  argv/hello field** (KH-2.3 forward-compat, defaulted). Because the broker
  observes every respawn this is strictly more reliable than brain→brain.
- The **same spawn-time channel carries Q4's update-vs-crash discriminator**
  (one channel, both payloads): the broker *initiated* a planned cycle but
  *observed* an unexpected crash.

### D4 — Multi-session resume; retire the `BrainState` message · Q6

Under the self-exit trigger the outgoing brain is gone before the new one
starts, so continuity must come from the persistent side, not a brain→brain
frame.

- The **broker becomes cursor-of-record** per session-subscriber (it already
  holds the per-session output ring, `broker.rs:73-143`): track each
  subscriber's last-**delivered** cursor.
- On (re)start the new brain queries the broker for **all** hosted sessions
  and re-attaches each in **resume** mode — fixing today's two gaps:
  `applyhost.rs:239` re-attaches only `sessions.first()` (one session) with
  `from_seq=0` (re-replays the whole ring → duplicate output).
- **Output is at-least-once** (broker resumes from last-*sent*); **input/
  effects stay exactly-once** via the already-broker-owned `EffectJournal`.
  Matches the SPIKE-05 terminal-stream contract (a resize repaint already
  reorders/duplicates the stream; exactly-once output would need a per-chunk
  ack for a guarantee the stream doesn't make).
- The explicit **`BrainState` handoff *message* retires** for production: the
  new brain cold-starts and reconstructs continuity by querying the broker.
  (`brain.rs` `BrainState`/`handoff` stay available for tests; the production
  path stops passing the frame.)

### D5 — Durable absolute-deadline loop timing · Q4, V3, V4

Timing that must survive a swap lives as durable absolute-deadline state on
disk, rehydrated on every brain start — not in the (planned-only) handoff
snapshot, which a crash carries nothing of.

- **Phase-significant resident loops only [V4].** The **pulse loop**
  (`lifecycle.rs:486` `sleep_interruptible(pulse_period)`, phase-relative →
  resets on restart) converts to a disk-anchored deadline. The **idempotent
  pump cadences need NO conversion** — they already "stagger from everything
  due now" (`peerloop.rs:304`), catch-up/idempotent, restart-safe; converting
  them would reintroduce the per-loop writes Q4 minimized.
- **Mechanism:** persist `(anchor, interval)` **once** on a fresh/crash start;
  derive `next_fire = anchor + interval × ⌈max(0, now − anchor) / interval⌉` —
  **no per-fire writes**. An **update restart re-reads `(anchor, interval)`
  and keeps deriving** (phase preserved, lands mid-grid). A **crash restart is
  a fresh start** (rewrite `anchor = now`; phase reset acceptable). The brain
  distinguishes update-vs-crash via the D3 spawn-time discriminator.
- **One-shot deadlines (alarms): rule fixed here, machinery deferred [V3].**
  Persist the absolute `target-time` at creation; every start (update *or*
  crash) reads it and fires-if-due; **never reset on crash** ("remind me at
  3pm" is a commitment). But the daemon has **no one-shot consumer today**
  (alarms are legacy-listener-only, §7) — building it now ships untested dead
  code. The *rule* is specified this milestone; the *machinery* is built with
  the alarm port (activate-don't-pre-fail). Tracked in DEFERRED.

### D6 — Failure atomicity: readiness-gated auto-rollback · Q7, V1

A new brain that bricks *logic* (panics on boot) must self-heal without a
human — endpoints stay up (the broker holds them).

- **Bounded-retry → auto-rollback.** If the new brain fails to reach `ready`
  (re-attached all sessions + resumed loops) within a bounded number of boots
  / a healthy-run window, the broker **rolls back to the last-known-good
  binary** (`spt.exe.old-N`), **quarantines** the bad version (no auto
  re-apply/re-fetch), and surfaces a loud consent-style notification. The
  broker holds **both** last-known-good + candidate paths and chooses which
  to spawn (no file manipulation at failure time). **Reuse** the existing
  readiness+backoff machinery: `peerloop.rs:82-91`
  `SUPERVISE_BACKOFF_BASE/CAP` + `SUPERVISE_HEALTHY_RUN` (60 s healthy resets
  backoff), `supervise_pump` wired at `peerloop.rs:805` (M8-D4).
- **Two-phase applied record** (fixes a real existing bug): `applyhost.rs:176`
  writes `record_applied(version)` + `last-outcome=applied` **before** the
  handoff — the optimistic `applied.json={version:6}` observed on `enlyzeam`.
  Make it: `applied-pending` at swap → promoted to `applied` on the `ready`
  signal → corrected to `rolled-back(quarantine=N, running=N-1)` on failure.
- **[V1] Rollback-state-compat invariant.** Auto-rollback spawns the **old**
  binary against durable state the **new** brain may already have written.
  Safe today (2026-06-09 audit: zero state-migration code), but the first
  durable-state schema migration would silently break rollback. Assert the
  invariant **now, while free**: a brain must not irreversibly migrate durable
  state before ready-promotion — every pre-ready write stays N-1-readable.
  This task lands the invariant as an explicit, tested guard/marker (and a
  doc note in KNOWN-HAZARDS 6.8) so a future migration trips a known wire.

### D7 — Proof + REQ re-pointing + fleet verify · V5, V6, §9

The milestone's conformance close-out.

- **Productionize SPIKE-01/03 as an `int` E2E [V5]:** a PTY child + a live
  QUIC conn survive a brain-**process** restart **onto a swapped binary** —
  pid unchanged, output stream gapless, conn intact. This is the test that
  proves *process-level* survival (the spikes proved the shape with two
  binaries; this proves it in production topology).
- **Re-point the regression-masked evidence.** REQ-DAEMON-2 and REQ-UPD-3's
  current `int` tags prove only the *in-process* handoff shape
  (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`, the M3b-B9 E2E) —
  mis-evidenced today. Move them to the D7 process-level E2E.
- **N-1 compat test as a CI gate [V6]:** the old-broker × new-brain
  verb-surface exercise scaffolded in D2 becomes a green gate.
- **Activate the minted hazards:** set real `required_stages` on
  `REQ-HAZARD-BROKER-PROCESS-ISOLATION` (doc=ADR-0018+design; impl/unit across
  D1–D6; int=the D7 E2E + N-1 test) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`
  (doc=ADR-0018+KH 6.8; impl/unit=D6 invariant guard).
- **Fleet verify** the real seamless update on the 3-node rig
  (kitsubito/hfenduleam/enlyzeam): publish a restoration+1 test release →
  `apply` on each node → **the running pid changes / new code goes live with
  no manual bounce**, every hosted endpoint survives. This is the acceptance
  the v0.3.2 roll could not get for free.

---

## Traceability — REQ activation schedule

| REQ | Today | This milestone | Activated at |
|-----|-------|----------------|--------------|
| `REQ-HAZARD-BROKER-PROCESS-ISOLATION` | minted, `required_stages=[]` | doc → impl → unit → int | doc now (ADR-0018+design exist); impl/unit across D1–D6; **int at D7** (process-level E2E + N-1 test) |
| `REQ-HAZARD-ROLLBACK-STATE-COMPAT` | minted, `required_stages=[]` | doc → impl → unit | doc now (ADR-0018+KH 6.8); **impl/unit at D6** (invariant guard) |
| `REQ-UPD-3` (no endpoint drop, brain-only) | `int` proves in-process shape (mis-evidenced) | re-point `int` to process-level survival | **D7** |
| `REQ-DAEMON-2` (broker/brain split for seamless update) | `int` proves in-process shape | re-point `int` to process-level survival | **D7** |

Rule 5 (activate-don't-pre-fail): toml `required_stages` are set at the
commit that delivers each stage's evidence, **not** now — `traceable-reqs
check` stays green at every commit. Re-pointing REQ-DAEMON-2/UPD-3 happens in
the same D7 commit that lands the replacement E2E (never leave them evidenced
by the old in-process test once the new test exists).

## Per-task gates (every commit)

`cargo build` · `cargo test` · `cargo clippy` · `cargo build
--no-default-features` · `traceable-reqs check` (EXIT=0) · `xtask check` (doc
drift). Push to a dev-freeform branch → **CI both runners** before any
release tag (the v0.3.0 lesson: CI catches regressions a local box can't —
single-home non-elevated dev box is blind to service-detection / socket-path
issues). The `[twohost]` rig rungs gate D7.

## Risks / watch-items

- **The hardest invariant** (de-risk spike #1): a PTY child + open socket
  survive a brain-process restart. The spikes proved it; D1 must re-prove it
  in production topology before logic moves (D3+). If D1's acceptance fails,
  stop — the whole design rests on it.
- **N-1 verb drift (KH-2.3).** Every verb added in D2 must default/additive;
  the D7 N-1 gate is the backstop. A breaking verb signature change is a
  broker-breaking update class — out of scope, must be refused, not silently
  shipped.
- **Test broker socket-bind flake** (DEFERRED, 2026-06-09): the AF_UNIX
  `served_broker` bind flakes on kitsubito. D1/D7 add real process-spawn
  tests on that runner — fix the bind determinism (unlink-before-bind /
  abstract namespace / per-test TempDir) opportunistically here rather than
  fighting flakes through the milestone.
- **Generation counter (KH-2.4):** custody moves to the broker (V2). Verify no
  consumer reads `gen_start_ms` as anything but observability (it already
  must not — `brain.rs:9-15`).

## Immediate next-session start

1. **D1 first** — add the `spt daemon brain` hidden entry + flip `spt daemon
   run` from `thread::spawn(broker.serve)` to broker-process + spawned brain
   child over the existing socket IPC. Prove the skeleton (broker survives
   brain kill, PTY child intact) **before** moving any logic.
2. Activate `REQ-HAZARD-BROKER-PROCESS-ISOLATION` doc stage (evidence already
   exists: ADR-0018 + design doc) in the D1 commit; impl/unit follow as D1
   lands real structure.
3. Keep `ensure_running`/`is_running`/`daemon stop` external behavior
   identical throughout — they're the single-instance contract the whole
   fleet depends on; only their *owner process* moves (thread → broker proc).
4. Do **not** convert the idempotent pump cadences (D5 [V4]) — only the pulse
   loop's phase-significant timing.


Bash

head -5 /c/Users/decid/Documents/projects/spt-core/docs/adr/0017-*.md; echo ---CHECKLIST---; sed -n '/Conformance checklist/,$p' /c/Users/decid/Documents/projects/spt-core/docs/KNOWN-HAZARDS.md | head -50; echo ---GATE---; traceable-reqs check 2>&1 | tail -15

ADR-0017 status convention + hazards checklist tail + traceable-reqs gate


# Subnet full-mesh via symmetric seed-proof membership + roster-only relay

## Status

accepted (2026-06-08) — amends ADR-0005 (pairing/trust) and ADR-0006 (multi-subnet membership); **preserves** KNOWN-HAZARDS 4.10 and 7.5 (does **not** supersede them — see Consequences). Design ratified in the 2026-06-08 grill-with-docs session (`SUBNET-MESH-PLAN.md` was the opening sketch; this ADR is the decided architecture).
---CHECKLIST---
## Conformance checklist (condensed)

| # | Invariant | spt-core surface |
|---|---|---|
| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |
| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |
| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
| 4.2 | Parser panic-free + tolerant | spt-proto |
| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |
| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |
| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
| 5.3 | Timeout every harness subprocess | manifest invocations |
| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |
| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |
| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |
| 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) |
---GATE---
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.


please vet the plans and new doc additions todlando wrote. message them if anything needs amendment: [Image #1]


Review broker brain split restoration plan