# Restoration D2 — IPC verb surface + loop migration (task plan)

> Working doc for RESTORATION-PLAN.md **D2** (ADR-0018 Q5/V6). The canonical
> loop→verb→moves-at-commit ledger lives in RESTORATION-PLAN.md D2; this doc
> holds the per-commit task detail. D1 (process-split skeleton) is DONE +
> cross-OS CI-green on main (0c95435).

## Goal (D2-close invariant)

Every logic loop runs in the **brain** child; the broker holds only continuity
resources (PTY masters / harness children / `EffectJournal` / NetHost / digest
hub / seed-control) + the verb server. D1 left every loop in the broker process;
D2 relocates them ONE PER COMMIT.

## Per-commit discipline

1. Define the loop's versioned verb (additive/defaulted — KH-2.3) **if** it made
   a direct `Arc<Broker>` call. A consumer that already spoke pure IPC needs no
   new verb — only its spawn site moves.
2. Reroute the direct call over the socket.
3. Move the loop into the brain child.
4. Prove the N-1 window (old-broker × new-brain) holds for the verb.

Gates every commit: `cargo build` · `cargo test` · `cargo clippy` ·
`cargo build --no-default-features` · `traceable-reqs check` (EXIT=0) ·
`xtask check`. Push to a dev-freeform branch → CI both runners before any tag.

---

## D2-1 — Net consumers → brain  ✅ DONE

**Cheapest first** (D1 discovery): the inbound dispatcher (`dispatch.rs`) and
the outbound peer pump (`peerloop.rs`) are ALREADY standalone IPC clients — each
`Brain::cold_start`s its own connection over `broker_socket_name()`, holds no
shared `Arc<Broker>`. They were merely *spawned* in the broker process for
convenience (`daemon.rs::spawn_net_consumers`). The migration is moving that
spawn site into the brain. **No new verb.**

Net-gating: consumers are meaningful only with a NetHost up. The brain polls the
existing `net-status` verb (answered `enabled:false` on a net-less broker, and
it carries `node_id_hex`) and starts the consumers exactly once, when net first
reports enabled — which also covers the boot-race self-heal (REQ-DAEMON-9): the
broker still owns the NetHost bind retry + `attach_net`, and the brain notices
`enabled` flip and starts consumers then. The broker's `net_retry_attach` keeps
the bind retry, drops the consumer spawn.

Why this is strictly better than today: consumers used to live in the broker, so
a (future) brain restart would not touch them and a broker restart killed them;
now they live with the brain, restart with it, and a brain respawn re-spawns
them from the swapped binary — the intended topology.

Changes:
- `brainproc.rs`: brain heartbeat polls `net-status`; on the first `enabled`
  reply, spawn the net consumers (moved `spawn_net_consumers` here, brain-side)
  with `node_id_hex` from the reply. `net-status` doubles as the broker-liveness
  probe (replaces the `sessions()` probe). `consumer_gate` pure helper enforces
  spawn-exactly-once.
- `daemon.rs`: drop both `spawn_net_consumers` call sites (boot `net_up` branch
  + inside `net_retry_attach`); `net_retry_attach` keeps the NetHost bind retry
  + `attach_net`, no longer spawns consumers (param `node_hex` retired).
- unit: `consumer_gate` once-only + net-gating table.

Evidence: `[impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]` (brain hosts the
consumers; broker no longer does) · `[unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
(the gate table). REQ already `[doc,impl,unit]` active — no toml change.

---

## D2-2 — Shellwake wake-host → brain  ✅ DONE

`shellwake::spawn_wake_host` is the offline half of shell online/offline
exclusivity: a 5 s reconcile sweep that runs each offline instance's
`wake_command` as a supervised watcher child. Same cheap shape as D2-1 — it
takes only a `stop` flag (no shared `Arc<Broker>`) and reaches the broker only
over the same socket IPC (`Brain::cold_start`) for the remote-wake dial. Q5
names it a **brain-owned** exception; its boot sweep re-reconciles the watcher
children from disk on every brain start, so a brain respawn re-sweeps orphaned
watchers — exactly the Q5 posture.

Changes:
- `brainproc.rs`: `run_brain` spawns the wake host once at brain start (before
  the heartbeat loop), unconditionally (no net dependency).
- `daemon.rs`: drop the broker-side `spawn_wake_host` block.

Evidence: `[impl->REQ-SHELL-2]` + `[impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]`.

## Scope correction — what is NOT a D2 relocation

A `Daemon::run` audit (D2-2) found the design's loop inventory over-counted what
the production spt-core daemon actually hosts:

- **Digest parse feed** (`Brain::run_digest_feed`): has **no production spawn
  site** — `ingest_chunk` is reached only from it, and the broker's `DigestHub`
  is empty in prod. It is an unwired brain-side primitive. Wiring it (the brain
  pushes parsed digests to the broker's hub over a **new additive verb**) is
  forward live-agent work, not a relocation of a D1-left loop.
- **Psyche / pulse loops** (`lifecycle.rs`): not spawned in `Daemon::run` at all
  — the live-agent loops still run in the legacy listener (owl.exe). They arrive
  in the spt-core daemon with the live-agent adapter buildout, on the post-D7
  final topology. The pulse loop's swap-durable timing is D5's concern when it
  does land here.

So D2's structural relocation is **complete** after D2-1 + D2-2: every logic
loop that actually runs in the broker process now runs in the brain.

## V6 — N-1 compat test scaffold → deferred to D3 (decision)

A CI-real old-broker-binary × new-brain-binary exercise across the verb surface.
The design slotted the scaffold in D2, but **D2 changed zero verbs** — both
migrations (D2-1 net consumers, D2-2 shellwake) only relocate a *spawn site*, so
there is no N-1 delta for the harness to assert. Building it now would test that
the *unchanged* verb set round-trips against itself — no teeth.

**Decision (2026-06-09): build the V6 harness in D3.** D3 lands the first real
versioned surface change — the spawn-time hello/argv field carrying
`{generation, start-reason}` + the update-vs-crash discriminator (ADR-0018
Q2/Q4, V2). That is the first additive field whose N-1 (old-broker × new-brain)
window must be proven, so the harness gets its first genuine assertion there and
grows with each later additive field; D7 makes it the green gate. Tracked in the
D3 task list.
