# Restoration D1 — Process split skeleton (JIT plan)

Parent: `RESTORATION-PLAN.md` D1 · ADR-0018 Q8 · de-risk spike #1.

## Scope (true minimal skeleton)

Prove the **process boundary** without moving any logic. `spt daemon run`
becomes the broker process and spawns a supervised `spt daemon brain` child;
the child connects over the existing broker socket, signals `ready`, and
idles. Every existing loop (net consumers, digest hub, shellwake, seed
control) **stays in the broker process** — they migrate in D2.

**Code reality check (investigation 2026-06-09):** there is no resident brain
process today. `Brain` (`brain.rs`) is a per-consumer *client* type
(`Role::Brain` hello) that api callers / live agents instantiate to drive
sessions. `spt daemon run` = broker + net-consumers + digest-hub + shellwake
threads. The net consumers already connect over `broker_socket_name()` (not a
shared `Arc`), so D1's brain child is a genuine no-op placeholder: its only
job is to prove broker-spawns-supervised-child-over-socket survives a child
kill. That is exactly the de-risk.

## Changes

1. **`endpoint.rs`** — add `brain_ready_path()` (`<spt_home>/brain.ready`,
   epoch-ms breadcrumb) — the D1 `ready` signal (D6 generalizes it).
2. **`brainproc.rs`** (new module) —
   - `run_brain()`: the `spt daemon brain` child entry. Connect to the broker
     (`Brain::cold_start`, bounded connect-retry — the broker may still be
     binding), write the ready breadcrumb, then idle: hold the connection,
     heartbeat the breadcrumb, exit non-zero if the broker connection drops
     (so the supervisor respawns). No de-elevation guard (inherits the
     already-unelevated broker token as a plain child).
   - `supervise_brain(stop, base, spawn_child)`: process-level mirror of
     `peerloop::supervise_pump` — spawn the child, wait, on exit log + respawn
     after capped doubling backoff (reuse the `next_backoff` shape), healthy
     run resets the floor. Generic over a `spawn_child: FnMut() -> io::Result<Child>`
     so the unit test injects a fake.
   - `spawn_brain_supervisor(stop) -> JoinHandle`: production wiring — spawns
     `current_exe daemon brain` via plain `Command` (inherits env incl.
     `SPT_HOME`; NOT `spawn_detached` — it's a managed child).
3. **`daemon.rs` `Daemon::run()`** — after the existing setup, before the
   foreground `serve_seed_control`, call `spawn_brain_supervisor(stop)` on a
   background thread. Add `Daemon::run_brain()` delegating to
   `brainproc::run_brain()`.
4. **`lib.rs`** — `pub mod brainproc;`.
5. **`cli.rs`** — hidden `DaemonCmd::Brain` variant → `cmd_daemon_brain()` →
   `spt_daemon::Daemon::run_brain()`. Hidden from help (`#[command(hide = true)]`).

## Traceability

- Activate `REQ-HAZARD-BROKER-PROCESS-ISOLATION` **doc** stage (evidence:
  ADR-0018 + design doc already exist) + **impl** (D1 structure) + **unit**
  (the supervisor respawn/backoff test). `int` waits for D7.
  `required_stages = ["doc", "impl", "unit"]` activated this commit (int added
  at D7).
- Tag: `// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]` on the supervisor +
  child entry; `// [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]` on the tests;
  `<!-- [doc->...] -->` already in ADR/design.

## Tests

- **Unit (`brainproc.rs`):** `supervise_brain` respawn — inject a `spawn_child`
  that counts calls + returns a child that exits immediately; assert it
  respawns N times under a tiny base backoff, and `stop` ends the loop.
  Backoff table reused/parallel to `peerloop::next_backoff`.
- **Process-level smoke (`crates/spt/tests/brain_split.rs`):** spawn the real
  `spt daemon run` (`CARGO_BIN_EXE_spt`) under a temp `SPT_HOME`; wait for the
  `brain.ready` breadcrumb (brain child came up); read+kill the brain child
  pid; assert the breadcrumb is rewritten (respawn) AND the seed channel still
  answers `is_running` (broker survived the brain death). This is the D1
  acceptance / de-risk. Tagged `[unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
  (process-level int is the D7 PTY+QUIC survival E2E).

## Acceptance

`spt daemon run` → a broker process + a distinct brain-child pid; killing the
brain leaves the broker (+ seed channel) alive and the supervisor respawns the
brain. No CLI behavior change (the broker still serves every loop).

## Gate

build · test · clippy · `--no-default-features` · `traceable-reqs check` ·
`xtask check`. Then dev-freeform push → CI both runners before any tag.
