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

# M3 Plan — Terminal wrapper + daemon + self-update

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b-PLAN.md`. The
> ordered task layer between `ROADMAP.md` (milestone sequence) and
> `traceable-reqs.toml` (requirement checklist). Honors ROADMAP §"lightweight but
> structured, GSD too heavy". Branch: `dev-freeform`.

> **M3 is split** (decision 2026-06-01): the milestone carries three large,
> separable deliverables plus a mandatory spike-gate. Building them as one
> undifferentiated task list would bury the broker/brain boundary the whole
> architecture rests on. The split mirrors M2a/M2b and ADR-0004's own layering:
> - **Phase 0 — spike-gate** (BLOCKING): close ADR-0004 §E before building the daemon.
> - **M3a — `spt-term`**: the broker's PTY/session-surface half (REQ-TERM-*).
> - **M3b — `spt-daemon`**: the broker/brain split + consolidation; re-hosts the
>   M2b lifecycle (REQ-DAEMON-*, REQ-START-3).
> - **M3c — self-update**: the gated, signed, ripple update engine (REQ-UPD-*,
>   REQ-SEAM-UPDATE). Peer-propagation *transport* defers to M4 (needs P2P).

## Goal

Replace the interim no-daemon model with the **real architecture** (ADR-0004):
one per-machine `spt-daemon` (broker/brain split) that owns all per-machine state
and re-hosts the M2b lifecycle as in-process loops with daemon-authoritative
liveness; a `spt-term` broker hosting PTYs behind a versioned local IPC; and a
**seamless self-update** that swaps the brain without any endpoint noticing. This
is where the two surviving FATALs land (ADR-0004 daemon, the broker↔brain
boundary) — the milestone that earns every interim seam its keep.

**M3 done =** the daemon hosts the LiveAgent lifecycle (Psyche-as-loop, no
`api listen` process, no seed file); `spt-term` hosts a real PTY with the
ConPTY-DSR hazard closed; a brain-only self-update swaps the brain with **zero**
endpoint interruption (the no-terminate invariant, proven E2E); adapters
ripple-update behind it; `cargo test --workspace` + clippy `-D warnings` green;
all M3-phase reqs activated; CI matrix (ubuntu + windows) green.

## Why now (what M0–M2b deferred here)

Every interim mechanism M1/M2a/M2b shipped is a documented swap point — M3 swaps
the *backing*, not the *shape*:

| Interim (M1/M2a/M2b) | M3 swap | Req / hazard |
|---|---|---|
| `api listen` process holds perch + relays | daemon-hosted loop + thin killable relay (topology 1) | REQ-DAEMON-1, ADR-0004 |
| seed-file bridge (`spt-store::seed`) | in-memory seed; spt-hosted spawn→bind (no file) | REQ-START-3 |
| interim pulse in the listen runner (`spt-live::pulse`) | daemon Psyche-loop with durable, configurable period | REQ-DAEMON-1 |
| per-pid Psyche liveness (`spt-live::psyche`) | daemon-authoritative `status` field | REQ-HAZARD-DAEMON-HOSTED-LIVENESS (2.5) |
| graceful-only signoff (`spt-live::signoff`) | always-on orphan-watch reusing the *same* grace/echo ordering | hazards 1.1 / 3.3 (supervised path) |
| `[inject]` `pty` method dropped (M2a) | real PTY injection (send-keys / send-line) | REQ-TERM-2 |

## Scope

### Phase 0 — spike-gate (BLOCKING; `spt-spikes/`, not in-repo crates)

ADR-0004 §E: four gaps **must close before M3 builds the daemon**. Spikes live in
`spt-spikes/` (the sandbox, per Spike #1/#2), not the spt-core workspace.

- **Spike #3 — QUIC + file-transfer stream survival across brain restart**
  (FATAL #2; *highest remaining risk in the whole roadmap*). Spike #1 proved
  PTY + plain TCP; QUIC carries process-local crypto/stream state. Stand up an
  iroh endpoint + an active QUIC stream + an in-flight file transfer in the
  broker, restart the brain, prove the stream survives. **De-risks the broker's
  ownership table (ADR-0004 §B) before the daemon is built around it.** Uses iroh
  in the sandbox only — M3 ships no networking; the *shape* is what's validated.
- **Spike #4 — Linux `forkpty` parity.** Spike #1 was Windows-only (the high-risk
  ConPTY path). Rerun the broker handoff on Linux `forkpty`.
- **Spike #5 — 100× restart + resize-under-load stress.** Spike #1 did one
  restart; harden before trusting the broker as the stable kernel.
- **Spike #6 — idempotent delivery across the broker↔brain boundary** (#14):
  prototype durable IDs + replay rules at each side-effect boundary (spool, PTY
  write, transfer, registry) — the design input for `REQ-HAZARD-RESTART-IDEMPOTENT`.

**Gate rule:** M3b does not start until #3 and #6 return a verdict. #4/#5 may run
in parallel with M3a. A spike that *falsifies* ADR-0004 §B re-opens the ADR
before any daemon code (cheaper than discovering it in the build).

### M3a — `spt-term` (the broker's PTY / session-surface half)

**In:** the `spt-term` crate (sibling of `spt-runtime`, above `spt-msg`):
- the **process-supervisor terminal wrapper** hosting broker PTYs (`REQ-TERM-1`)
  — ConPTY on Windows / `forkpty` on Unix behind one `SessionSurface` trait;
- the **session-surface abstraction** with `send-keys` + `send-line` injection
  (`REQ-TERM-2`) — the real PTY channel the M2a `[inject]` `pty` method (dropped
  then) now resolves to;
- **byte-stream remote terminal streaming** for v1 (`REQ-TERM-3`) — the local
  stream shape (the off-node hop is M4 transport);
- the **ConPTY-DSR auto-answer** (`REQ-HAZARD-CONPTY-DSR` / 5.5) — every ConPTY
  reader answers `ESC[6n` → cursor report; reader drains on a thread, never gates
  exit on a blocking `read()`. (Pattern already proven in Spike #1.)

**Out:** the broker *process* + IPC (that's M3b — `spt-term` provides the PTY
*mechanism* the broker hosts); off-node terminal transport (M4).

### M3b — `spt-daemon` (broker/brain split + consolidation)

**In:** the `spt-daemon` crate (above `spt-live`, below `spt`):
- **broker** (stable kernel) — holds only un-transferable resources per the
  ADR-0004 §B ownership table: PTY master handles (`spt-term`), spawned harness
  child processes, accepted local client sockets, listening sockets. Minimal
  **versioned local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3 — newer brain ↔
  older broker).
- **brain** (userspace) — all logic: routing, registry, **the re-hosted M2b
  lifecycle** (Psyche-as-loop + the durable pulse period replacing the interim
  glue), manifest parse. Restarts freely; rehydrates from disk and re-attaches to
  broker-held handles. `gen_start = now()` on cold-start **and** handoff
  (`REQ-HAZARD-GEN-START-NOW` / 2.4).
- **consolidation** (`REQ-DAEMON-1`) — poll-listener + Psyche/pulse loops move
  into the daemon; the only residue is the thin **stateless in-session relay**
  for harness-hosted sessions (topology 1), freely killable.
- **daemon-authoritative liveness** (`REQ-HAZARD-DAEMON-HOSTED-LIVENESS` / 2.5) —
  the daemon's `status` field supersedes `is_process_alive(info.pid)`; the M2b
  per-pid checks gated behind the interim seam become the swap.
- **idempotent delivery** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2) — durable IDs +
  replay rules at every broker↔brain side-effect boundary (from Spike #6).
- **auto-start** (`REQ-DAEMON-3`) — any `api` invocation auto-starts the daemon
  if absent.
- **spt-hosted startup** (`REQ-START-3`) — spawn-session then `api bind`, in-memory
  seed, no seed file (swaps the M2a interim `spt-store::seed`).
- **orphan-watch** — carry the M2b graceful ordering (grace 1.1 / echo 3.3 /
  stale-sweep 3.2) into the **supervised crash** path the always-on daemon now
  enables (M2b delivered graceful-only).

**Out:** networking/P2P (M4); the QUIC-stream broker ownership *implementation*
(M4 — but its *shape* validated by Spike #3 now).

### M3c — self-update (gated, signed, ripple)

**In:**
- the **update-class taxonomy** (ADR-0004 §A): **brain-only** (no endpoint
  terminates — `REQ-UPD-3`, absolute, the routine case, proven E2E),
  **broker-compatible** (hot-swap behind the versioned IPC), **broker-breaking**
  (planned endpoint-cycle, may suspend with consent + scheduling);
- **signature verification before handoff** (`REQ-UPD-2`) — spt-core's release
  key, on every binary regardless of source;
- **update hardening** (`REQ-HAZARD-UPDATE-ROLLBACK`, ADR-0004 §D) — monotonic
  version (rollback rejection), release-metadata expiry, channel pinning, key
  rotation/revocation list, **adapter content signing**;
- **consent gating** (`REQ-UPD-4`) — gated on user confirmation delivered to the
  most-recently-active live session; opt-in full-auto;
- **adapter ripple-update** (`REQ-UPD-5` + `REQ-SEAM-UPDATE`) — spt-core conducts
  the stack: self first, then each registered adapter via its manifest `[update]`
  avenue (file-pull / delegated command), with adapter content signing.

**Out:**
- **Peer-propagation transport over P2P** (`REQ-UPD-1`) — needs the M4 network
  layer. M3c ships the update *engine* with **self-fetch / out-of-band** delivery
  (ADR-0004: "layered on self-fetch, out-of-band still supported"); peer-propagation
  is the M4 transport plugged into the same engine. `REQ-UPD-1` activates at M4.

## Clean-room posture (validated against the sister)

- **`spt-term` is brand-new** — the sister never hosted ConPTY/PTY directly (it
  shelled out to `claude`). Clean-room the whole crate; the only copied artifact
  is the **ConPTY-DSR mechanism** proven in Spike #1 (harness-orthogonal).
- **`spt-daemon` consolidation** — copy the sister's hard-won **ordering /
  liveness invariants** (orphan grace 1.1, echo 3.3, stale-sweep 3.2, stable-PID
  anchor 5.1, gen_start 2.4) and clean-room the process model (the sister's
  separate listener + wrapper processes collapse into the one daemon — ADR-0004).
- **self-update** — clean-room (the sister used a GitHub remote + `gh`; spt-core
  self-fetches + verifies signatures, P2P transport at M4 — ADR-0002/0004).

## New requirements to register first (TRACEABILITY rule 3)

**Assessment — none.** Every M3 deliverable maps onto already-registered reqs:
- daemon → `REQ-DAEMON-1/2/3/4`; terminal → `REQ-TERM-1/2/3`; startup →
  `REQ-START-3`; update → `REQ-UPD-2/3/4/5`, `REQ-SEAM-UPDATE`; `REQ-UPD-1`
  (peer-propagation) stays `[]` until M4.
- hazards → `REQ-HAZARD-CONPTY-DSR` (5.5), `REQ-HAZARD-DAEMON-HOSTED-LIVENESS`
  (2.5), `REQ-HAZARD-RESTART-IDEMPOTENT` (7.2), `REQ-HAZARD-HANDOFF-ARGV-COMPAT`
  (2.3), `REQ-HAZARD-GEN-START-NOW` (2.4), `REQ-HAZARD-UPDATE-ROLLBACK` — all
  registered; activate per task.
- `REQ-DAEMON-4` ("honor every KNOWN-HAZARDS invariant") is the conformance
  umbrella — activate last (M3 close), evidenced by the hazard suite being green.

## Sequencing rationale

Spike-gate first (§E gaps falsify-or-confirm the broker shape *before* code).
Then `spt-term` (the PTY mechanism the broker hosts — buildable in parallel with
spikes #4/#5). Then `spt-daemon`: broker skeleton (host `spt-term` PTYs +
versioned IPC) → brain skeleton (rehydrate + re-attach) → **re-host the M2b
lifecycle** (the load-bearing consolidation) → daemon liveness → idempotent
boundary → orphan-watch → spt-hosted startup → auto-start. Self-update last (it
rests on the broker/brain split being real): update-class engine → signature +
rollback hardening → consent gating → adapter ripple. Each task tags evidence +
activates its reqs in the same commit; `traceable-reqs check` green before the next.

## Tasks — Phase 0 spike-gate (`spt-spikes/`)

| # | Task | Source | Gate | Acceptance |
|---|------|--------|------|------------|
| S3 | **Spike #3 — QUIC + file-transfer survival across brain restart** | ADR-0004 §E.1 (FATAL #2) | blocks M3b | an active QUIC stream + in-flight transfer in the broker survive a brain restart; or ADR-0004 §B re-opened |
| S4 | **Spike #4 — Linux `forkpty` parity** | ADR-0004 §E.2 | blocks M3a close | broker PTY handoff passes on Linux `forkpty` (mirror Spike #1) |
| S5 | **Spike #5 — 100× restart + resize-under-load** | ADR-0004 §E.3 | blocks M3b close | 100 brain restarts + concurrent resizes, no leak/hang/lost-byte |
| S6 | **Spike #6 — idempotent broker↔brain delivery** | ADR-0004 §E.4 | blocks M3b idempotency task | durable-ID + replay rule prototype: crash before/after each side-effect, no dup/drop |

## Tasks — M3a `spt-term`

| # | Task | Reqs / hazards | Acceptance |
|---|------|----------------|------------|
| A0 | Scaffold `crates/spt-term` (sibling of spt-runtime, above spt-msg); deps proto/store/msg | — | compiles; layering acyclic; `check` green |
| A1 | `SessionSurface` trait + ConPTY (win) / `forkpty` (unix) backends; spawn a child under a PTY | REQ-TERM-1 | a child runs under a real PTY on both OSes; output captured |
| A2 | **ConPTY-DSR auto-answer**: reader answers `ESC[6n`→cursor report on a drain thread | REQ-HAZARD-CONPTY-DSR (5.5) | a ConPTY child's stdout is NOT withheld; DSR query auto-answered (regression test) |
| A3 | `send-keys` + `send-line` injection over the surface | REQ-TERM-2 | injected keys/lines reach the child; ordering preserved |
| A4 | Byte-stream local terminal streaming (v1 shape) | REQ-TERM-3 | a consumer streams the child's byte output; backpressure bounded |
| A5 | Activation sweep (REQ-TERM-1/2/3 + CONPTY-DSR) | — | `check` green; CI matrix green |

## Tasks — M3b `spt-daemon`

| # | Task | Reqs / hazards | Acceptance |
|---|------|----------------|------------|
| B0 | Scaffold `crates/spt-daemon` (above spt-live, below spt) | — | compiles; layering acyclic |
| B1 | **Broker** skeleton: host `spt-term` PTYs + spawned children + sockets; minimal **versioned** local IPC | REQ-DAEMON-2; REQ-HAZARD-HANDOFF-ARGV-COMPAT (2.3) | newer brain talks to older broker across an IPC version bump |
| B2 | **Brain** skeleton: rehydrate from disk + re-attach to broker handles; `gen_start=now()` cold-start & handoff | REQ-DAEMON-2; REQ-HAZARD-GEN-START-NOW (2.4) | brain restart re-attaches; fresh gen_start each start |
| B3 | **Re-host the M2b lifecycle** into the brain: Psyche-as-loop + durable pulse period (replace `spt-live::pulse` interim glue + the `api listen` host) | REQ-DAEMON-1 | the daemon hosts the lifecycle; the interim `api listen` host path retired |
| B4 | **Daemon-authoritative liveness**: `status` field supersedes per-pid checks | REQ-HAZARD-DAEMON-HOSTED-LIVENESS (2.5) | a daemon-hosted endpoint reports live with no dedicated pid; M2b per-pid seam swapped |
| B5 | **Idempotent delivery** across the broker↔brain boundary (durable IDs + replay) | REQ-HAZARD-RESTART-IDEMPOTENT (7.2) | crash before/after spool/PTY/registry side-effects → exactly-once |
| B6 | **Orphan-watch**: supervised crash path reuses M2b grace/echo ordering | hazards 1.1 / 3.3 / 3.2 (supervised) | an orphaned Self after harness death is grace-rechecked + echo-before-signoff |
| B7 | **spt-hosted startup**: spawn-session→`api bind`, in-memory seed (no file) | REQ-START-3 | a spt-hosted session binds with no seed file |
| B8 | **Auto-start**: any `api` call auto-starts the daemon if absent | REQ-DAEMON-3 | a cold `api` call launches the daemon then proceeds |
| B9 | Daemon lifecycle E2E + activation (DAEMON-1/2/3, START-3, the 4 hazards) | — | E2E: brain restart preserves a live PTY session; `check` green |

## Tasks — M3c self-update

| # | Task | Reqs / hazards | Acceptance |
|---|------|----------------|------------|
| C0 | Update-class engine: brain-only / broker-compatible / broker-breaking dispatch | ADR-0004 §A | each class routed to its invariant; brain-only never touches endpoints |
| C1 | **Signature verify before handoff** (release key) + monotonic version / expiry / channel-pin / revocation | REQ-UPD-2; REQ-HAZARD-UPDATE-ROLLBACK | unsigned/older/expired/revoked rejected; valid swap proceeds |
| C2 | **Brain-only swap E2E — the no-terminate invariant** | REQ-UPD-3 | a brain-only update swaps the brain with ZERO endpoint interruption (live PTY survives) |
| C3 | **Consent gating**: confirm to most-recently-active live session; opt-in full-auto | REQ-UPD-4 | update blocks on consent by default; full-auto opt-in honored |
| C4 | **Adapter ripple-update** via manifest `[update]` avenue + adapter content signing | REQ-UPD-5; REQ-SEAM-UPDATE | self updates then each registered adapter ripples; adapter payload signature-checked |
| C5 | Activation sweep: REQ-UPD-2/3/4/5 + SEAM-UPDATE + UPDATE-ROLLBACK; **REQ-DAEMON-4** (hazard-suite umbrella); amend ROADMAP (M3 delivered) + CONTEXT/M3 notes; author M4 plan stub | — | `check` green with all M3 reqs activated; CI matrix green |

## M3 requirement-activation map

Activate as each task lands (default `["impl","unit"]`; daemon/term/update E2Es
add `int`):
- **Terminal:** REQ-TERM-1 (A1), REQ-TERM-2 (A3), REQ-TERM-3 (A4); REQ-HAZARD-CONPTY-DSR (A2).
- **Daemon:** REQ-DAEMON-2 (B1/B2), REQ-DAEMON-1 (B3; +int B9), REQ-DAEMON-3 (B8),
  REQ-START-3 (B7; +int B9); hazards HANDOFF-ARGV-COMPAT (B1), GEN-START-NOW (B2),
  DAEMON-HOSTED-LIVENESS (B4), RESTART-IDEMPOTENT (B5).
- **Update:** REQ-UPD-2 (C1), REQ-UPD-3 (C2; +int), REQ-UPD-4 (C3), REQ-UPD-5 (C4),
  REQ-SEAM-UPDATE (C4); REQ-HAZARD-UPDATE-ROLLBACK (C1).
- **Umbrella:** REQ-DAEMON-4 (C5) — last, evidenced by the full hazard suite green.

**Stay `[]` (M4):** `REQ-UPD-1` (peer-propagation transport), all `R-NET`/`R-PAIR`,
`REQ-EP-4` (PresenceChannel impl), the distributed precedence/freshness
generalization of 6.5 (node identity).

## Workspace change

Add `crates/spt-term` (sibling of `spt-runtime`) and `crates/spt-daemon` (above
`spt-live`). Final layering (R-ARCH-1, acyclic) matches the day-one comment:

```
spt-proto → spt-store → spt-msg → {spt-net, spt-term, spt-runtime} → spt-live → spt-daemon → spt
```

`spt` becomes a **thin client** of `spt-daemon` (auto-starts + talks to it).
Neither `spt-term` nor `spt-daemon` is public SDK (R-ARCH-2 stays
proto/runtime/msg) — they are the per-machine host the daemon consolidates.
`spt-net` (M4) slots in as the remaining sibling.

## Risks carried into M3

- **The two FATALs live here.** ADR-0004 (daemon broker/brain) is the keystone;
  the interim model existed precisely so M3 is a controlled consolidation, not a
  leap. **Do not start M3b until the spike-gate (Phase 0) returns** — a falsified
  ownership table is cheap to fix before code, ruinous after.
- **QUIC-across-restart is unproven (Spike #3) — the single highest risk in the
  roadmap.** It validates the broker's most invasive resource ownership. If it
  fails, ADR-0004 §B re-opens before any daemon line is written.
- **No-terminate is an *absolute* only for brain-only** (ADR-0004 §A). Don't let
  the C2 E2E quietly downgrade to "endpoints usually survive" — it must be zero
  interruption, asserted against a live PTY session.
- **Re-host, not rewrite.** The M2b seams (seed-file, pulse module, per-pid
  liveness, graceful signoff) are deliberately localized — M3 swaps their
  *backing*, not their *shape*. A task that finds itself rewriting a seam's
  contract is a signal the interim boundary was wrong; surface it, don't paper
  over it.
- **Update signing is mandatory, not optional** (ADR-0004 §D / peer-propagation
  makes one compromised node a subnet poison vector). Adapter content signing
  (not just binary signing) is the easy-to-miss half — C4 must cover it.
- **Cross-milestone coupling.** `REQ-UPD-1` (peer-propagation) and the QUIC-stream
  broker *implementation* both need M4's P2P. M3 ships the update *engine* + the
  broker *shape* (spike-validated); M4 plugs in the transport. Keep the update
  engine and broker ownership table transport-agnostic so M4 is an integration,
  not a re-architecture.
```

-# M3 Plan (stub) — Terminal wrapper + daemon + self-update

+# M3 Plan — Terminal wrapper + daemon + self-update

-> **Stub, not a JIT plan.** Per the M0/M1/M2a/M2b cadence the full task breakdown

-> is authored *when M3 starts* (activate reqs as each milestone task lands —

-> "activate, don't pre-fail"). This file captures **scope + the carried-forward

-> obligations** so M3 begins with the gaps already named. Branch: `dev-freeform`.

+> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b-PLAN.md`. The

+> ordered task layer between `ROADMAP.md` (milestone sequence) and

+> `traceable-reqs.toml` (requirement checklist). Honors ROADMAP §"lightweight but

+> structured, GSD too heavy". Branch: `dev-freeform`.

+> **M3 is split** (decision 2026-06-01): the milestone carries three large,

+> separable deliverables plus a mandatory spike-gate. Building them as one

+> undifferentiated task list would bury the broker/brain boundary the whole

+> architecture rests on. The split mirrors M2a/M2b and ADR-0004's own layering:

+> - **Phase 0 — spike-gate** (BLOCKING): close ADR-0004 §E before building the daemon.

+> - **M3a — `spt-term`**: the broker's PTY/session-surface half (REQ-TERM-*).

+> - **M3b — `spt-daemon`**: the broker/brain split + consolidation; re-hosts the

+>   M2b lifecycle (REQ-DAEMON-*, REQ-START-3).

+> - **M3c — self-update**: the gated, signed, ripple update engine (REQ-UPD-*,

+>   REQ-SEAM-UPDATE). Peer-propagation *transport* defers to M4 (needs P2P).

-Consolidate the interim no-daemon model into the **real architecture**: one

-per-machine `spt-daemon` (broker/brain split, ADR-0004) that owns all

-per-machine state and re-hosts the M2b lifecycle (Psyche-as-loop,

-daemon-authoritative liveness), a `spt-term` broker hosting PTYs (the

-session-surface + ConPTY/forkpty layer), and peer-propagated **signed**

-self-update. This is where the two FATALs land (ADR-0004 daemon, the broker↔brain

-boundary) — the milestone that earns the interim seams their keep.

+Replace the interim no-daemon model with the **real architecture** (ADR-0004):

+one per-machine `spt-daemon` (broker/brain split) that owns all per-machine state

+and re-hosts the M2b lifecycle as in-process loops with daemon-authoritative

+liveness; a `spt-term` broker hosting PTYs behind a versioned local IPC; and a

+**seamless self-update** that swaps the brain without any endpoint noticing. This

+is where the two surviving FATALs land (ADR-0004 daemon, the broker↔brain

+boundary) — the milestone that earns every interim seam its keep.

+**M3 done =** the daemon hosts the LiveAgent lifecycle (Psyche-as-loop, no

+`api listen` process, no seed file); `spt-term` hosts a real PTY with the

+ConPTY-DSR hazard closed; a brain-only self-update swaps the brain with **zero**

+endpoint interruption (the no-terminate invariant, proven E2E); adapters

+ripple-update behind it; `cargo test --workspace` + clippy `-D warnings` green;

+all M3-phase reqs activated; CI matrix (ubuntu + windows) green.

-M1/M2a/M2b deliberately ran an **interim** model: the `api listen` process holds

-the perch + relays, the seed bridges via a file, and the Psyche is a real

-subprocess with per-pid liveness. Every one of those is a documented swap point:

+Every interim mechanism M1/M2a/M2b shipped is a documented swap point — M3 swaps

-- **seed-file** (M2a `spt-store::seed`) → daemon in-memory seed.

-- **`api listen` host loop + interim pulse** (M2b `spt::api::live` / `spt-live::pulse`)

-  → daemon-hosted Psyche loop with the durable, configurable pulse period.

-- **per-pid Psyche liveness** (M2b `spt-live::psyche`) → daemon-authoritative

-  `status` field (KNOWN-HAZARDS 2.5 / `REQ-HAZARD-DAEMON-HOSTED-LIVENESS`).

-- **graceful-only signoff** (M2b `spt-live::signoff`) → the always-on

-  **orphan-watch** reusing the *same* ordering (grace 1.1 / echo 3.3) on the

+| Interim (M1/M2a/M2b) | M3 swap | Req / hazard |

+| `api listen` process holds perch + relays | daemon-hosted loop + thin killable relay (topology 1) | REQ-DAEMON-1, ADR-0004 |

+| seed-file bridge (`spt-store::seed`) | in-memory seed; spt-hosted spawn→bind (no file) | REQ-START-3 |

+| interim pulse in the listen runner (`spt-live::pulse`) | daemon Psyche-loop with durable, configurable period | REQ-DAEMON-1 |

+| per-pid Psyche liveness (`spt-live::psyche`) | daemon-authoritative `status` field | REQ-HAZARD-DAEMON-HOSTED-LIVENESS (2.5) |

+| graceful-only signoff (`spt-live::signoff`) | always-on orphan-watch reusing the *same* grace/echo ordering | hazards 1.1 / 3.3 (supervised path) |

+| `[inject]` `pty` method dropped (M2a) | real PTY injection (send-keys / send-line) | REQ-TERM-2 |

-Keep the M3 move a **re-host, not a rewrite** — the seams are already thin.

-**In (the sixth+ layers — `spt-term`, `spt-daemon`; `spt` becomes a thin client):**

-- **`spt-term`** — `REQ-TERM-1/2/3`: the process-supervisor terminal wrapper

-  hosting broker PTYs; the session-surface abstraction (send-keys / send-line

-  injection — the M2a `[inject]` `pty` method that was dropped becomes real);

-  byte-stream remote terminal streaming for v1.

-- **`spt-daemon`** — `REQ-DAEMON-1/2/3/4`: one per-machine daemon owning all

-  per-machine state; the broker/brain split (ADR-0004); re-hosts the M2b

-  lifecycle; daemon-authoritative liveness; in-memory seed (swap `REQ-START-1`'s

-  file). **`REQ-START-3`** spt-hosted startup (PTY-launch, no seed file).

-- **Self-update** — `REQ-UPD-1/2/3/4/5` + `REQ-SEAM-UPDATE`: peer-propagated

-  update over P2P; all binaries signature-verified before handoff; no endpoint

-  terminates/suspends during update; user-confirmation-gated by default;

-  spt-core ripple-updates registered adapters.

+### Phase 0 — spike-gate (BLOCKING; `spt-spikes/`, not in-repo crates)

-**Hazards M3 must satisfy (each needs a test before "covered"):**

-- `REQ-HAZARD-DAEMON-HOSTED-LIVENESS` (2.5) — daemon `status` authoritative.

-- `REQ-HAZARD-CONPTY-DSR` (5.5) — every ConPTY reader auto-answers DSR

-  (`ESC[6n` → cursor report); already reproduced + fixed-pattern in Spike #1.

-- `REQ-HAZARD-RESTART-IDEMPOTENT` (7.2) — exactly-once side effects across the

-  broker↔brain boundary (spool, PTY write, transfer, registry).

-- Carry the M2b ordering hazards (1.1/3.3/3.2) into the **supervised** orphan

-  path (not just the graceful path M2b delivered).

+ADR-0004 §E: four gaps **must close before M3 builds the daemon**. Spikes live in

+`spt-spikes/` (the sandbox, per Spike #1/#2), not the spt-core workspace.

-- **Cross-node Psyche sync + distributed precedence/freshness** (the node-id

-  generalization of 6.5) — M4 (ADR-0003). M2b landed the *local* guard.

-- **P2P networking proper / pairing** (ADR-0002/0005, R-NET/R-PAIR) — M4/M5.

-- The **memformat / Memory-Health / block-OCC** context-store evolution —

-  deferred (`docs/DEFERRED.md`); M2b ingests communes as opaque blobs.

+- **Spike #3 — QUIC + file-transfer stream survival across brain restart**

+  (FATAL #2; *highest remaining risk in the whole roadmap*). Spike #1 proved

+  PTY + plain TCP; QUIC carries process-local crypto/stream state. Stand up an

+  iroh endpoint + an active QUIC stream + an in-flight file transfer in the

+  broker, restart the brain, prove the stream survives. **De-risks the broker's

+  ownership table (ADR-0004 §B) before the daemon is built around it.** Uses iroh

+  in the sandbox only — M3 ships no networking; the *shape* is what's validated.

+- **Spike #4 — Linux `forkpty` parity.** Spike #1 was Windows-only (the high-risk

+  ConPTY path). Rerun the broker handoff on Linux `forkpty`.

+- **Spike #5 — 100× restart + resize-under-load stress.** Spike #1 did one

+  restart; harden before trusting the broker as the stable kernel.

+- **Spike #6 — idempotent delivery across the broker↔brain boundary** (#14):

+  prototype durable IDs + replay rules at each side-effect boundary (spool, PTY

+  write, transfer, registry) — the design input for `REQ-HAZARD-RESTART-IDEMPOTENT`.

-## Carried-forward obligations (Spike #1 — promote to M3 tasks)

+**Gate rule:** M3b does not start until #3 and #6 return a verdict. #4/#5 may run

+in parallel with M3a. A spike that *falsifies* ADR-0004 §B re-opens the ADR

+before any daemon code (cheaper than discovering it in the build).

-The broker/brain split survived Stage A (ConPTY passes), but four gaps were

-explicitly deferred into this plan (`docs/spikes/SPIKE-01-broker-handoff.md`,

+### M3a — `spt-term` (the broker's PTY / session-surface half)

-1. **Live Iroh/QUIC + file-transfer stream survival across brain restart**

-   (codex FATAL #2) — *highest remaining risk*. Plain-TCP survival is a weak

-   proxy; QUIC carries process-local crypto/stream state. Spike this first.

-2. **Linux `forkpty` parity** — Spike #1 was Windows-only.

-3. **100× restart + resize-under-load** — only a single restart was proven.

-4. **Idempotent delivery** across the broker↔brain boundary (codex #14 /

+**In:** the `spt-term` crate (sibling of `spt-runtime`, above `spt-msg`):

+- the **process-supervisor terminal wrapper** hosting broker PTYs (`REQ-TERM-1`)

+  — ConPTY on Windows / `forkpty` on Unix behind one `SessionSurface` trait;

+- the **session-surface abstraction** with `send-keys` + `send-line` injection

+  (`REQ-TERM-2`) — the real PTY channel the M2a `[inject]` `pty` method (dropped

+- **byte-stream remote terminal streaming** for v1 (`REQ-TERM-3`) — the local

+  stream shape (the off-node hop is M4 transport);

+- the **ConPTY-DSR auto-answer** (`REQ-HAZARD-CONPTY-DSR` / 5.5) — every ConPTY

+  reader answers `ESC[6n` → cursor report; reader drains on a thread, never gates

+  exit on a blocking `read()`. (Pattern already proven in Spike #1.)

+**Out:** the broker *process* + IPC (that's M3b — `spt-term` provides the PTY

+*mechanism* the broker hosts); off-node terminal transport (M4).

-Scaffold `spt-term` (PTY/session-surface) → broker PTY hosting + the ConPTY-DSR

-hazard test → `spt-daemon` skeleton (broker/brain split) → re-host the M2b

-lifecycle (Psyche-as-loop, daemon liveness, in-memory seed) → orphan-watch

-(supervised grace/echo ordering) → idempotent-delivery boundary → self-update

-(signed, gated, ripple-to-adapters). Spike the QUIC-restart-survival gap (#1)

-**before** committing the broker↔brain transport shape. Each task tags evidence

-+ activates its reqs in the same commit; `traceable-reqs check` green before the

+### M3b — `spt-daemon` (broker/brain split + consolidation)

+**In:** the `spt-daemon` crate (above `spt-live`, below `spt`):

+- **broker** (stable kernel) — holds only un-transferable resources per the

+  ADR-0004 §B ownership table: PTY master handles (`spt-term`), spawned harness

+  child processes, accepted local client sockets, listening sockets. Minimal

+  **versioned local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3 — newer brain ↔

+- **brain** (userspace) — all logic: routing, registry, **the re-hosted M2b

+  lifecycle** (Psyche-as-loop + the durable pulse period replacing the interim

+  glue), manifest parse. Restarts freely; rehydrates from disk and re-attaches to

+  broker-held handles. `gen_start = now()` on cold-start **and** handoff

+- **consolidation** (`REQ-DAEMON-1`) — poll-listener + Psyche/pulse loops move

+  into the daemon; the only residue is the thin **stateless in-session relay**

+  for harness-hosted sessions (topology 1), freely killable.

+- **daemon-authoritative liveness** (`REQ-HAZARD-DAEMON-HOSTED-LIVENESS` / 2.5) —

+  the daemon's `status` field supersedes `is_process_alive(info.pid)`; the M2b

+  per-pid checks gated behind the interim seam become the swap.

+- **idempotent delivery** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2) — durable IDs +

+  replay rules at every broker↔brain side-effect boundary (from Spike #6).

+- **auto-start** (`REQ-DAEMON-3`) — any `api` invocation auto-starts the daemon

+- **spt-hosted startup** (`REQ-START-3`) — spawn-session then `api bind`, in-memory

+  seed, no seed file (swaps the M2a interim `spt-store::seed`).

+- **orphan-watch** — carry the M2b graceful ordering (grace 1.1 / echo 3.3 /

+  stale-sweep 3.2) into the **supervised crash** path the always-on daemon now

+  enables (M2b delivered graceful-only).

+**Out:** networking/P2P (M4); the QUIC-stream broker ownership *implementation*

+(M4 — but its *shape* validated by Spike #3 now).

+### M3c — self-update (gated, signed, ripple)

+- the **update-class taxonomy** (ADR-0004 §A): **brain-only** (no endpoint

+  terminates — `REQ-UPD-3`, absolute, the routine case, proven E2E),

+  **broker-compatible** (hot-swap behind the versioned IPC), **broker-breaking**

+  (planned endpoint-cycle, may suspend with consent + scheduling);

+- **signature verification before handoff** (`REQ-UPD-2`) — spt-core's release

+  key, on every binary regardless of source;

+- **update hardening** (`REQ-HAZARD-UPDATE-ROLLBACK`, ADR-0004 §D) — monotonic

+  version (rollback rejection), release-metadata expiry, channel pinning, key

+  rotation/revocation list, **adapter content signing**;

+- **consent gating** (`REQ-UPD-4`) — gated on user confirmation delivered to the

+  most-recently-active live session; opt-in full-auto;

+- **adapter ripple-update** (`REQ-UPD-5` + `REQ-SEAM-UPDATE`) — spt-core conducts

+  the stack: self first, then each registered adapter via its manifest `[update]`

+  avenue (file-pull / delegated command), with adapter content signing.

+- **Peer-propagation transport over P2P** (`REQ-UPD-1`) — needs the M4 network

+  layer. M3c ships the update *engine* with **self-fetch / out-of-band** delivery

+  (ADR-0004: "layered on self-fetch, out-of-band still supported"); peer-propagation

+  is the M4 transport plugged into the same engine. `REQ-UPD-1` activates at M4.

+## Clean-room posture (validated against the sister)

+- **`spt-term` is brand-new** — the sister never hosted ConPTY/PTY directly (it

+  shelled out to `claude`). Clean-room the whole crate; the only copied artifact

+  is the **ConPTY-DSR mechanism** proven in Spike #1 (harness-orthogonal).

+- **`spt-daemon` consolidation** — copy the sister's hard-won **ordering /

+  liveness invariants** (orphan grace 1.1, echo 3.3, stale-sweep 3.2, stable-PID

+  anchor 5.1, gen_start 2.4) and clean-room the process model (the sister's

+  separate listener + wrapper processes collapse into the one daemon — ADR-0004).

+- **self-update** — clean-room (the sister used a GitHub remote + `gh`; spt-core

+  self-fetches + verifies signatures, P2P transport at M4 — ADR-0002/0004).

+## New requirements to register first (TRACEABILITY rule 3)

+**Assessment — none.** Every M3 deliverable maps onto already-registered reqs:

+- daemon → `REQ-DAEMON-1/2/3/4`; terminal → `REQ-TERM-1/2/3`; startup →

+  `REQ-START-3`; update → `REQ-UPD-2/3/4/5`, `REQ-SEAM-UPDATE`; `REQ-UPD-1`

+  (peer-propagation) stays `[]` until M4.

+- hazards → `REQ-HAZARD-CONPTY-DSR` (5.5), `REQ-HAZARD-DAEMON-HOSTED-LIVENESS`

+  (2.5), `REQ-HAZARD-RESTART-IDEMPOTENT` (7.2), `REQ-HAZARD-HANDOFF-ARGV-COMPAT`

+  (2.3), `REQ-HAZARD-GEN-START-NOW` (2.4), `REQ-HAZARD-UPDATE-ROLLBACK` — all

+- `REQ-DAEMON-4` ("honor every KNOWN-HAZARDS invariant") is the conformance

+  umbrella — activate last (M3 close), evidenced by the hazard suite being green.

+Spike-gate first (§E gaps falsify-or-confirm the broker shape *before* code).

+Then `spt-term` (the PTY mechanism the broker hosts — buildable in parallel with

+spikes #4/#5). Then `spt-daemon`: broker skeleton (host `spt-term` PTYs +

+versioned IPC) → brain skeleton (rehydrate + re-attach) → **re-host the M2b

+lifecycle** (the load-bearing consolidation) → daemon liveness → idempotent

+boundary → orphan-watch → spt-hosted startup → auto-start. Self-update last (it

+rests on the broker/brain split being real): update-class engine → signature +

+rollback hardening → consent gating → adapter ripple. Each task tags evidence +

+activates its reqs in the same commit; `traceable-reqs check` green before the next.

+## Tasks — Phase 0 spike-gate (`spt-spikes/`)

+| # | Task | Source | Gate | Acceptance |

+| S3 | **Spike #3 — QUIC + file-transfer survival across brain restart** | ADR-0004 §E.1 (FATAL #2) | blocks M3b | an active QUIC stream + in-flight transfer in the broker survive a brain restart; or ADR-0004 §B re-opened |

+| S4 | **Spike #4 — Linux `forkpty` parity** | ADR-0004 §E.2 | blocks M3a close | broker PTY handoff passes on Linux `forkpty` (mirror Spike #1) |

+| S5 | **Spike #5 — 100× restart + resize-under-load** | ADR-0004 §E.3 | blocks M3b close | 100 brain restarts + concurrent resizes, no leak/hang/lost-byte |

+| S6 | **Spike #6 — idempotent broker↔brain delivery** | ADR-0004 §E.4 | blocks M3b idempotency task | durable-ID + replay rule prototype: crash before/after each side-effect, no dup/drop |

+| # | Task | Reqs / hazards | Acceptance |

+| A0 | Scaffold `crates/spt-term` (sibling of spt-runtime, above spt-msg); deps proto/store/msg | — | compiles; layering acyclic; `check` green |

+| A1 | `SessionSurface` trait + ConPTY (win) / `forkpty` (unix) backends; spawn a child under a PTY | REQ-TERM-1 | a child runs under a real PTY on both OSes; output captured |

+| A2 | **ConPTY-DSR auto-answer**: reader answers `ESC[6n`→cursor report on a drain thread | REQ-HAZARD-CONPTY-DSR (5.5) | a ConPTY child's stdout is NOT withheld; DSR query auto-answered (regression test) |

+| A3 | `send-keys` + `send-line` injection over the surface | REQ-TERM-2 | injected keys/lines reach the child; ordering preserved |

+| A4 | Byte-stream local terminal streaming (v1 shape) | REQ-TERM-3 | a consumer streams the child's byte output; backpressure bounded |

+| A5 | Activation sweep (REQ-TERM-1/2/3 + CONPTY-DSR) | — | `check` green; CI matrix green |

+| B0 | Scaffold `crates/spt-daemon` (above spt-live, below spt) | — | compiles; layering acyclic |

+| B1 | **Broker** skeleton: host `spt-term` PTYs + spawned children + sockets; minimal **versioned** local IPC | REQ-DAEMON-2; REQ-HAZARD-HANDOFF-ARGV-COMPAT (2.3) | newer brain talks to older broker across an IPC version bump |

+| B2 | **Brain** skeleton: rehydrate from disk + re-attach to broker handles; `gen_start=now()` cold-start & handoff | REQ-DAEMON-2; REQ-HAZARD-GEN-START-NOW (2.4) | brain restart re-attaches; fresh gen_start each start |

+| B3 | **Re-host the M2b lifecycle** into the brain: Psyche-as-loop + durable pulse period (replace `spt-live::pulse` interim glue + the `api listen` host) | REQ-DAEMON-1 | the daemon hosts the lifecycle; the interim `api listen` host path retired |

+| B4 | **Daemon-authoritative liveness**: `status` field supersedes per-pid checks | REQ-HAZARD-DAEMON-HOSTED-LIVENESS (2.5) | a daemon-hosted endpoint reports live with no dedicated pid; M2b per-pid seam swapped |

+| B5 | **Idempotent delivery** across the broker↔brain boundary (durable IDs + replay) | REQ-HAZARD-RESTART-IDEMPOTENT (7.2) | crash before/after spool/PTY/registry side-effects → exactly-once |

+| B6 | **Orphan-watch**: supervised crash path reuses M2b grace/echo ordering | hazards 1.1 / 3.3 / 3.2 (supervised) | an orphaned Self after harness death is grace-rechecked + echo-before-signoff |

+| B7 | **spt-hosted startup**: spawn-session→`api bind`, in-memory seed (no file) | REQ-START-3 | a spt-hosted session binds with no seed file |

+| B8 | **Auto-start**: any `api` call auto-starts the daemon if absent | REQ-DAEMON-3 | a cold `api` call launches the daemon then proceeds |

+| B9 | Daemon lifecycle E2E + activation (DAEMON-1/2/3, START-3, the 4 hazards) | — | E2E: brain restart preserves a live PTY session; `check` green |

+| # | Task | Reqs / hazards | Acceptance |

+| C0 | Update-class engine: brain-only / broker-compatible / broker-breaking dispatch | ADR-0004 §A | each class routed to its invariant; brain-only never touches endpoints |

+| C1 | **Signature verify before handoff** (release key) + monotonic version / expiry / channel-pin / revocation | REQ-UPD-2; REQ-HAZARD-UPDATE-ROLLBACK | unsigned/older/expired/revoked rejected; valid swap proceeds |

+| C2 | **Brain-only swap E2E — the no-terminate invariant** | REQ-UPD-3 | a brain-only update swaps the brain with ZERO endpoint interruption (live PTY survives) |

+| C3 | **Consent gating**: confirm to most-recently-active live session; opt-in full-auto | REQ-UPD-4 | update blocks on consent by default; full-auto opt-in honored |

+| C4 | **Adapter ripple-update** via manifest `[update]` avenue + adapter content signing | REQ-UPD-5; REQ-SEAM-UPDATE | self updates then each registered adapter ripples; adapter payload signature-checked |

+| C5 | Activation sweep: REQ-UPD-2/3/4/5 + SEAM-UPDATE + UPDATE-ROLLBACK; **REQ-DAEMON-4** (hazard-suite umbrella); amend ROADMAP (M3 delivered) + CONTEXT/M3 notes; author M4 plan stub | — | `check` green with all M3 reqs activated; CI matrix green |

+Activate as each task lands (default `["impl","unit"]`; daemon/term/update E2Es

+- **Terminal:** REQ-TERM-1 (A1), REQ-TERM-2 (A3), REQ-TERM-3 (A4); REQ-HAZARD-CONPTY-DSR (A2).

+- **Daemon:** REQ-DAEMON-2 (B1/B2), REQ-DAEMON-1 (B3; +int B9), REQ-DAEMON-3 (B8),

+  REQ-START-3 (B7; +int B9); hazards HANDOFF-ARGV-COMPAT (B1), GEN-START-NOW (B2),

+  DAEMON-HOSTED-LIVENESS (B4), RESTART-IDEMPOTENT (B5).

+- **Update:** REQ-UPD-2 (C1), REQ-UPD-3 (C2; +int), REQ-UPD-4 (C3), REQ-UPD-5 (C4),

+  REQ-SEAM-UPDATE (C4); REQ-HAZARD-UPDATE-ROLLBACK (C1).

+- **Umbrella:** REQ-DAEMON-4 (C5) — last, evidenced by the full hazard suite green.

+**Stay `[]` (M4):** `REQ-UPD-1` (peer-propagation transport), all `R-NET`/`R-PAIR`,

+`REQ-EP-4` (PresenceChannel impl), the distributed precedence/freshness

+Add `crates/spt-term` (sibling of `spt-runtime`) and `crates/spt-daemon` (above

+`spt-live`). Final layering (R-ARCH-1, acyclic) matches the day-one comment:

+spt-proto → spt-store → spt-msg → {spt-net, spt-term, spt-runtime} → spt-live → spt-daemon → spt

+`spt` becomes a **thin client** of `spt-daemon` (auto-starts + talks to it).

+Neither `spt-term` nor `spt-daemon` is public SDK (R-ARCH-2 stays

+proto/runtime/msg) — they are the per-machine host the daemon consolidates.

+`spt-net` (M4) slots in as the remaining sibling.

-- **The two FATALs live here** (ADR-0004 daemon, ADR-0005 pairing is M4). The

-  daemon is the architecture's keystone; the interim model exists precisely so

-  this milestone is a controlled consolidation, not a leap.

-- **QUIC-across-restart is unproven** (gap #1) — the highest single risk in the

-  whole roadmap. De-risk with a spike before the transport is load-bearing.

-- **Don't let the re-host become a rewrite.** The M2b seams (seed-file, pulse

-  module, per-pid liveness, graceful signoff) are deliberately localized — M3

-  swaps their *backing*, not their *shape*.

+- **The two FATALs live here.** ADR-0004 (daemon broker/brain) is the keystone;

+  the interim model existed precisely so M3 is a controlled consolidation, not a

+  leap. **Do not start M3b until the spike-gate (Phase 0) returns** — a falsified

+  ownership table is cheap to fix before code, ruinous after.

+- **QUIC-across-restart is unproven (Spike #3) — the single highest risk in the

+  roadmap.** It validates the broker's most invasive resource ownership. If it

+  fails, ADR-0004 §B re-opens before any daemon line is written.

+- **No-terminate is an *absolute* only for brain-only** (ADR-0004 §A). Don't let

+  the C2 E2E quietly downgrade to "endpoints usually survive" — it must be zero

+  interruption, asserted against a live PTY session.

+- **Re-host, not rewrite.** The M2b seams (seed-file, pulse module, per-pid

+  liveness, graceful signoff) are deliberately localized — M3 swaps their

+  *backing*, not their *shape*. A task that finds itself rewriting a seam's

+  contract is a signal the interim boundary was wrong; surface it, don't paper

+- **Update signing is mandatory, not optional** (ADR-0004 §D / peer-propagation

+  makes one compromised node a subnet poison vector). Adapter content signing

+  (not just binary signing) is the easy-to-miss half — C4 must cover it.

+- **Cross-milestone coupling.** `REQ-UPD-1` (peer-propagation) and the QUIC-stream

+  broker *implementation* both need M4's P2P. M3 ships the update *engine* + the

+  broker *shape* (spike-validated); M4 plugs in the transport. Keep the update

+  engine and broker ownership table transport-agnostic so M4 is an integration,

# M3 Plan (stub) — Terminal wrapper + daemon + self-update

> **Stub, not a JIT plan.** Per the M0/M1/M2a/M2b cadence the full task breakdown
> is authored *when M3 starts* (activate reqs as each milestone task lands —
> "activate, don't pre-fail"). This file captures **scope + the carried-forward
> obligations** so M3 begins with the gaps already named. Branch: `dev-freeform`.

## Goal

Consolidate the interim no-daemon model into the **real architecture**: one
per-machine `spt-daemon` (broker/brain split, ADR-0004) that owns all
per-machine state and re-hosts the M2b lifecycle (Psyche-as-loop,
daemon-authoritative liveness), a `spt-term` broker hosting PTYs (the
session-surface + ConPTY/forkpty layer), and peer-propagated **signed**
self-update. This is where the two FATALs land (ADR-0004 daemon, the broker↔brain
boundary) — the milestone that earns the interim seams their keep.

## Why now (what M0–M2b deferred here)

M1/M2a/M2b deliberately ran an **interim** model: the `api listen` process holds
the perch + relays, the seed bridges via a file, and the Psyche is a real
subprocess with per-pid liveness. Every one of those is a documented swap point:

- **seed-file** (M2a `spt-store::seed`) → daemon in-memory seed.
- **`api listen` host loop + interim pulse** (M2b `spt::api::live` / `spt-live::pulse`)
  → daemon-hosted Psyche loop with the durable, configurable pulse period.
- **per-pid Psyche liveness** (M2b `spt-live::psyche`) → daemon-authoritative
  `status` field (KNOWN-HAZARDS 2.5 / `REQ-HAZARD-DAEMON-HOSTED-LIVENESS`).
- **graceful-only signoff** (M2b `spt-live::signoff`) → the always-on
  **orphan-watch** reusing the *same* ordering (grace 1.1 / echo 3.3) on the
  supervised crash path.

Keep the M3 move a **re-host, not a rewrite** — the seams are already thin.

## Scope

**In (the sixth+ layers — `spt-term`, `spt-daemon`; `spt` becomes a thin client):**
- **`spt-term`** — `REQ-TERM-1/2/3`: the process-supervisor terminal wrapper
  hosting broker PTYs; the session-surface abstraction (send-keys / send-line
  injection — the M2a `[inject]` `pty` method that was dropped becomes real);
  byte-stream remote terminal streaming for v1.
- **`spt-daemon`** — `REQ-DAEMON-1/2/3/4`: one per-machine daemon owning all
  per-machine state; the broker/brain split (ADR-0004); re-hosts the M2b
  lifecycle; daemon-authoritative liveness; in-memory seed (swap `REQ-START-1`'s
  file). **`REQ-START-3`** spt-hosted startup (PTY-launch, no seed file).
- **Self-update** — `REQ-UPD-1/2/3/4/5` + `REQ-SEAM-UPDATE`: peer-propagated
  update over P2P; all binaries signature-verified before handoff; no endpoint
  terminates/suspends during update; user-confirmation-gated by default;
  spt-core ripple-updates registered adapters.

**Hazards M3 must satisfy (each needs a test before "covered"):**
- `REQ-HAZARD-DAEMON-HOSTED-LIVENESS` (2.5) — daemon `status` authoritative.
- `REQ-HAZARD-CONPTY-DSR` (5.5) — every ConPTY reader auto-answers DSR
  (`ESC[6n` → cursor report); already reproduced + fixed-pattern in Spike #1.
- `REQ-HAZARD-RESTART-IDEMPOTENT` (7.2) — exactly-once side effects across the
  broker↔brain boundary (spool, PTY write, transfer, registry).
- Carry the M2b ordering hazards (1.1/3.3/3.2) into the **supervised** orphan
  path (not just the graceful path M2b delivered).

**Out (later milestones):**
- **Cross-node Psyche sync + distributed precedence/freshness** (the node-id
  generalization of 6.5) — M4 (ADR-0003). M2b landed the *local* guard.
- **P2P networking proper / pairing** (ADR-0002/0005, R-NET/R-PAIR) — M4/M5.
- The **memformat / Memory-Health / block-OCC** context-store evolution —
  deferred (`docs/DEFERRED.md`); M2b ingests communes as opaque blobs.

## Carried-forward obligations (Spike #1 — promote to M3 tasks)

The broker/brain split survived Stage A (ConPTY passes), but four gaps were
explicitly deferred into this plan (`docs/spikes/SPIKE-01-broker-handoff.md`,
ADR-0004 §E):

1. **Live Iroh/QUIC + file-transfer stream survival across brain restart**
   (codex FATAL #2) — *highest remaining risk*. Plain-TCP survival is a weak
   proxy; QUIC carries process-local crypto/stream state. Spike this first.
2. **Linux `forkpty` parity** — Spike #1 was Windows-only.
3. **100× restart + resize-under-load** — only a single restart was proven.
4. **Idempotent delivery** across the broker↔brain boundary (codex #14 /
   `REQ-HAZARD-RESTART-IDEMPOTENT`).

## Sequencing rationale (provisional)

Scaffold `spt-term` (PTY/session-surface) → broker PTY hosting + the ConPTY-DSR
hazard test → `spt-daemon` skeleton (broker/brain split) → re-host the M2b
lifecycle (Psyche-as-loop, daemon liveness, in-memory seed) → orphan-watch
(supervised grace/echo ordering) → idempotent-delivery boundary → self-update
(signed, gated, ripple-to-adapters). Spike the QUIC-restart-survival gap (#1)
**before** committing the broker↔brain transport shape. Each task tags evidence
+ activates its reqs in the same commit; `traceable-reqs check` green before the
next.

## Risks carried into M3

- **The two FATALs live here** (ADR-0004 daemon, ADR-0005 pairing is M4). The
  daemon is the architecture's keystone; the interim model exists precisely so
  this milestone is a controlled consolidation, not a leap.
- **QUIC-across-restart is unproven** (gap #1) — the highest single risk in the
  whole roadmap. De-risk with a spike before the transport is load-bearing.
- **Don't let the re-host become a rewrite.** The M2b seams (seed-file, pulse
  module, per-pid liveness, graceful signoff) are deliberately localized — M3
  swaps their *backing*, not their *shape*.


Fix the stray trailing code fence.


236	- **Terminal:** REQ-TERM-1 (A1), REQ-TERM-2 (A3), REQ-TERM-3 (A4); REQ-HAZARD-CONPTY-DSR (A2).
237	- **Daemon:** REQ-DAEMON-2 (B1/B2), REQ-DAEMON-1 (B3; +int B9), REQ-DAEMON-3 (B8),
238	  REQ-START-3 (B7; +int B9); hazards HANDOFF-ARGV-COMPAT (B1), GEN-START-NOW (B2),
239	  DAEMON-HOSTED-LIVENESS (B4), RESTART-IDEMPOTENT (B5).
240	- **Update:** REQ-UPD-2 (C1), REQ-UPD-3 (C2; +int), REQ-UPD-4 (C3), REQ-UPD-5 (C4),
241	  REQ-SEAM-UPDATE (C4); REQ-HAZARD-UPDATE-ROLLBACK (C1).
242	- **Umbrella:** REQ-DAEMON-4 (C5) — last, evidenced by the full hazard suite green.
243

- **Terminal:** REQ-TERM-1 (A1), REQ-TERM-2 (A3), REQ-TERM-3 (A4); REQ-HAZARD-CONPTY-DSR (A2).
- **Daemon:** REQ-DAEMON-2 (B1/B2), REQ-DAEMON-1 (B3; +int B9), REQ-DAEMON-3 (B8),
  REQ-START-3 (B7; +int B9); hazards HANDOFF-ARGV-COMPAT (B1), GEN-START-NOW (B2),
  DAEMON-HOSTED-LIVENESS (B4), RESTART-IDEMPOTENT (B5).
- **Update:** REQ-UPD-2 (C1), REQ-UPD-3 (C2; +int), REQ-UPD-4 (C3), REQ-UPD-5 (C4),
  REQ-SEAM-UPDATE (C4); REQ-HAZARD-UPDATE-ROLLBACK (C1).
- **Umbrella:** REQ-DAEMON-4 (C5) — last, evidenced by the full hazard suite green.


Bash

Check file tail for stray fence


  broker *implementation* both need M4's P2P. M3 ships the update *engine* + the
  broker *shape* (spike-validated); M4 plugs in the transport. Keep the update
  engine and broker ownership table transport-agnostic so M4 is an integration,
  not a re-architecture.
```


Edit

  engine and broker ownership table transport-agnostic so M4 is an integration,
  not a re-architecture.
```

  engine and broker ownership table transport-agnostic so M4 is an integration,
  not a re-architecture.


  engine and broker ownership table transport-agnostic so M4 is an integration,
  not a re-architecture.
```

broker *shape* (spike-validated); M4 plugs in the transport. Keep the update


Stray fence removed. Verify check + commit the full M3 plan.


Bash

traceable-reqs check > /dev/null 2>&1; echo "check_exit=$?"; git add -A && git commit -q -F - <<'EOF'
docs(M3): author M3-PLAN.md (terminal wrapper + daemon + self-update)

Promote the M2b-authored stub to the full JIT plan. M3 splits into a BLOCKING
spike-gate + three phases (mirrors M2a/M2b and ADR-0004's broker/brain/update
layering):

- Phase 0 spike-gate (spt-spikes/): close ADR-0004 §E before building the
  daemon — Spike #3 QUIC+transfer survival across brain restart (highest risk,
  blocks M3b), #4 Linux forkpty, #5 100x restart+resize, #6 idempotent boundary.
- M3a spt-term: SessionSurface (ConPTY/forkpty) + send-keys/line + byte stream +
  the ConPTY-DSR auto-answer (REQ-TERM-1/2/3, 5.5).
- M3b spt-daemon: broker/brain split (versioned IPC, 2.3/2.4), re-host the M2b
  lifecycle as Psyche-as-loop, daemon-authoritative liveness (2.5), idempotent
  boundary (7.2), orphan-watch (supervised 1.1/3.3/3.2), spt-hosted startup
  (START-3), auto-start (DAEMON-3).
- M3c self-update: update-class taxonomy (brain-only no-terminate absolute),
  signature+rollback hardening (UPD-2, UPDATE-ROLLBACK §D), consent gating
  (UPD-4), adapter ripple (UPD-5/SEAM-UPDATE). Peer-propagation transport
  (UPD-1) defers to M4 (needs P2P) — M3c ships the engine via self-fetch.

No new reqs (all map to registered DAEMON/TERM/UPD/START + the six M3 hazards).
Per-task activation map + the interim→daemon swap-point table included. Marks
the M2b seams as re-host (not rewrite) targets. ROADMAP M3 already links it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -1

Commit M3 plan


check_exit=0
warning: in the working copy of 'M3-PLAN.md', LF will be replaced by CRLF the next time Git touches it
610c995 docs(M3): author M3-PLAN.md (terminal wrapper + daemon + self-update)


`M3-PLAN.md` written + committed (610c995). check green.

**M3 plan — split into spike-gate + 3 phases** (mirrors M2a/M2b + ADR-0004 layering):

**Phase 0 — spike-gate (BLOCKING, `spt-spikes/`):** ADR-0004 §E says close before building daemon.
- S3 QUIC+transfer survival across brain restart — **highest roadmap risk, blocks M3b**
- S4 Linux forkpty · S5 100× restart+resize · S6 idempotent boundary

**M3a — `spt-term`** (PTY mechanism): `SessionSurface` (ConPTY/forkpty), send-keys/line, byte stream, **ConPTY-DSR auto-answer**. REQ-TERM-1/2/3 + 5.5. A0–A5.

**M3b — `spt-daemon`** (broker/brain split): versioned IPC (2.3/2.4), **re-host M2b lifecycle as Psyche-loop**, daemon-authoritative liveness (2.5), idempotent boundary (7.2), orphan-watch (supervised 1.1/3.3/3.2), spt-hosted startup (START-3), auto-start (DAEMON-3). B0–B9.

**M3c — self-update**: update-class taxonomy (brain-only = no-terminate absolute), signature+rollback hardening (UPDATE-ROLLBACK), consent gating, adapter ripple. C0–C5.

Key calls baked in:
- **Peer-propagation transport (UPD-1) → M4** (needs P2P). M3c ships the update *engine* via self-fetch; M4 plugs transport.
- **QUIC broker-ownership** validated by spike *shape* now, *implementation* M4.
- **No new reqs** — all map to registered DAEMON/TERM/UPD/START + 6 M3 hazards.
- M2b seams flagged **re-host, not rewrite**; swap-point table included.
- DAEMON-4 (hazard umbrella) activates last, evidenced by full suite green.

Entry point when M3 starts: Phase 0 spike-gate (S3 + S6 before M3b).