Write

# M3b Plan — `spt-daemon` broker/brain process

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a-PLAN.md`. The
> ordered task layer between `ROADMAP.md`, `M3-PLAN.md` (the M3 umbrella), and
> `traceable-reqs.toml` (the requirement checklist), scoped to **M3b only**.
> Branch: `dev-freeform`. Authoritative architecture: ADR-0004 (broker/brain split
> + the §B ownership table + the closed §E spike gaps) and `M3-PLAN.md` §M3b.

> **Upstream is done:** Phase 0 spike-gate ✅ (Spikes #3/#4/#5/#6 all PASS — ADR-0004
> §E closed on both OSes) and **M3a ✅ delivered** — `spt-term` provides the
> single-surface PTY *mechanism* (SessionSurface + ConPTY/forkpty backend + DSR
> drain + injection + bounded byte-stream + digest parser primitive). **M3b builds
> the *process* that hosts many of those surfaces** behind a versioned local IPC,
> consolidating the interim no-daemon model.

## Goal

Replace the interim no-daemon model (M1/M2a/M2b: a long-lived `spt api listen`
process holds the perch + drives a relay/pulse loop; a seed *file* bridges
binding; per-pid `is_process_alive` is liveness) with the **real architecture**
(ADR-0004): one per-machine `spt-daemon`, split into a **broker** (stable kernel
holding the un-transferable resources) and a **brain** (restartable logic), so a
routine self-update swaps the brain with **zero endpoint interruption**.

**M3b done =** the daemon hosts the M2b LiveAgent lifecycle as in-process loops
(Psyche-as-loop, durable pulse — no separate listener/wrapper process, only a thin
stateless in-session relay residue) · the broker hosts `spt-term` PTYs + harness
children + sockets behind a **versioned local IPC** that a newer brain speaks to an
older broker · **a brain kill/restart leaves the child + its live output stream
intact and gapless** (Spike #1 shape, made real) · liveness for daemon-hosted
perches is the daemon's `status`, never `is_process_alive(info.pid)` · every
broker↔brain side-effect boundary is **exactly-once** across a brain crash (Spike
#6) · any `api` invocation **auto-starts** the daemon · spt-hosted startup is
spawn-session→`api bind` with an **in-memory** seed (no file) · the **digest
daemon-half** completes REQ-TERM-4 (`int`) · `cargo test --workspace` + clippy
`-D warnings` + `traceable-reqs check` green on the CI matrix (ubuntu + windows).

## Scope

### In — the `spt-daemon` crate (above `spt-live`, below `spt`)

- **broker** (stable kernel, `REQ-DAEMON-2`) — owns **only** the resources a live
  consumer would lose continuity on if the brain restarted (ADR-0004 §B normative
  table): PTY master handles (**from `spt-term::PtySession`**), spawned harness
  child processes, accepted local client sockets, listening sockets. No logic.
  Almost never restarts.
- **brain** (userspace, `REQ-DAEMON-2`) — all logic: routing, registry, manifest
  parse, **the re-hosted M2b lifecycle**. Restarts freely; rehydrates from disk +
  re-attaches to broker-held handles over IPC. `gen_start = now()` on cold-start
  **and** handoff (`REQ-HAZARD-GEN-START-NOW` / 2.4).
- **versioned local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3) — named pipe
  (Windows) / Unix-domain socket, behind one transport trait (mirrors how
  `SessionSurface` abstracts the PTY OS-split). A version handshake + a
  forward-compatible frame schema so a **newer brain talks to an older broker**.
- **consolidation** (`REQ-DAEMON-1`) — the `spt api listen` poll-loop + the pulse
  loop + the Psyche loop move **into the brain** as scheduled in-process loops; the
  only residue is the thin **stateless in-session relay** for harness-hosted
  sessions (topology 1), freely killable. Durable, configurable pulse period
  replacing the interim 5s constant.
- **daemon-authoritative liveness** (`REQ-HAZARD-DAEMON-HOSTED-LIVENESS` / 2.5) —
  the daemon's in-memory endpoint table + a `status` field on `info.json`
  supersede `is_process_alive(info.pid)` for **daemon-hosted** perches; localized
  behind the single liveness resolver so external/interim perches still probe.
- **idempotent broker↔brain boundary** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2,
  Spike #6) — durable IDs + dedup-at-effect at every side-effect crossing (spool
  write, PTY write, registry update), with a **broker-owned recovery anchor**;
  exactly-once across a brain crash at before-intent / before-effect / after-effect.
- **auto-start** (`REQ-DAEMON-3`) — any `api` invocation starts the daemon if
  absent (`listen` is the reliable anchor); ensure-running lives in the `api` layer.
- **spt-hosted startup** (`REQ-START-3`) — daemon spawn-session into a broker PTY,
  then `api bind`; **in-memory** seed map replaces the `spt-store::seed` file bridge.
- **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).
- **digest daemon-half** (`REQ-TERM-4` `int`, ADR-0008) — manifest-sourcing of the
  `input`/`agent`/`tool` patterns (the `pty_digest` manifest seam), running the
  M3a `DigestParser` over the broker PTY; `spt digest <id>` snapshot pull, the
  broker-pushed structured **delta-stream** on the R-TERM-3 substrate, and opt-in
  Path-B persistence. Local addressing only (the qualified `[subnet:]id@node` form
  is M4); `REQ-DAEMON-4` (honor every KNOWN-HAZARDS invariant) is the meta-gate.

### Out

- networking / P2P, multi-subnet, notifications, **cross-node** digest addressing —
  **M4** (`spt-net`). The QUIC-stream broker-ownership *implementation* is M4
  (only its *shape* was spiked, #3).
- self-update (update-class taxonomy, signing, ripple) — **M3c**.
- whole-daemon live FD-passing (zero-interruption even for *broker* updates) —
  explicit future polish, not v1 (ADR-0004 consequences).

## Clean-room posture

- **`spt-daemon` is brand-new.** The sister ran poll listeners + Psyche wrappers as
  separate processes; ADR-0004 deliberately consolidates them. Clean-room the crate.
- **Re-host, don't rewrite.** The M2b lifecycle is already pure, unit-tested
  functions in `spt-live` (`spawn_psyche`, `resume_psyche`, `run_echo_commune`,
  `ingest_drops`, `graceful_signoff`, `sweep_stale_signoff`, `pulse::tick`,
  `fetch_history`, `write_resume_commune`, `compose_psyche_prompt`). The brain
  *calls* these as loops; the ordering invariants (3.3 / 1.1 / 3.2) stay where they
  are. **Reuse `spt-term::PtySession` for the broker's PTYs** — do not re-implement.
- **Spike findings are binding inputs:** #1 (no-EOF-while-writer-held + DSR; the
  broker-survives-brain-restart shape), #3 (live-stream survival shape), #6
  (broker-owned recovery anchor + dedup-at-effect keyed by durable ID).

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

**Assessment — likely none.** The M3b reqs already exist: `REQ-DAEMON-1/2/3/4`,
`REQ-START-3`, `REQ-HAZARD-HANDOFF-ARGV-COMPAT`, `REQ-HAZARD-GEN-START-NOW`,
`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`, `REQ-HAZARD-RESTART-IDEMPOTENT`, and
`REQ-TERM-4` (`int` stage). They sit `required_stages = []`; activate as each task
lands. **If** the versioned-IPC frame contract or the in-session relay turns out to
need its own conformance id during B0/B3, register it in `traceable-reqs.toml`
first, then satisfy it.

## Sequencing rationale

Scaffold + the **versioned IPC transport** (the load-bearing seam everything rides)
→ the **broker kernel** (owns resources, holds no logic) → the **brain + gapless
handoff** (the milestone's hardest proof: kill brain, child + stream survive) →
**consolidation** (fold the interim loops in) → **daemon liveness** (localized swap)
→ **idempotent boundary** (durable IDs at every effect) → **auto-start + in-memory
startup** (retire the seed file) → **orphan-watch** → **digest daemon-half** →
activation + E2E. Each task tags evidence + activates its reqs in the same commit;
`traceable-reqs check` green and **both OSes** validated before the next (the M3a
discipline: Windows local + Linux via gravity-linux).

## Tasks — `spt-daemon` (the broker/brain process)

| # | Task | Reqs / hazards | Acceptance |
|---|------|----------------|------------|
| B0 | **Scaffold `crates/spt-daemon`** (above spt-live, below spt; deps spt-store/msg/runtime/term/live) + the **versioned local-IPC transport**: a `DaemonTransport` trait over named-pipe (win) / UDS (unix), a length-framed message codec, and a **version handshake** with a forward-compatible frame schema | REQ-DAEMON-2 (foundation), REQ-HAZARD-HANDOFF-ARGV-COMPAT | crate compiles; layering `…→spt-live→spt-daemon→spt` acyclic; a brain frame with an unknown trailing field is accepted by an older broker (forward-compat unit test); `check` green |
| B1 | **Broker kernel**: spawn a child under a `spt-term::PtySession`; hold the PTY master + child + a listening/client socket; serve them over IPC (subscribe-to-output, write-input, resize, lifecycle). **No logic in the broker.** | REQ-DAEMON-2 (broker half) | a broker hosts one real PTY child on both OSes, streams its output + accepts injected input over IPC; the ADR-0004 §B ownership table is the only state it holds (review-asserted) |
| B2 | **Brain + gapless handoff**: brain connects over the versioned IPC, subscribes to the broker's PTY output, sends input; **a brain kill + restart re-attaches with no lost/dup output and the child untouched** (Spike #1 made real). `gen_start = now()` on cold-start **and** handoff | REQ-HAZARD-GEN-START-NOW, REQ-DAEMON-2 (brain half) | regression test: spawn child → brain attached → kill+restart brain ≥10× → child alive throughout, output stream contiguous across each restart, `gen_start` advances each handoff |
| B3 | **Consolidation**: fold the `api listen` poll-loop + pulse + Psyche loops into the brain as scheduled in-process loops (re-hosting the spt-live seams); leave only the thin **stateless in-session relay** for harness-hosted sessions. Durable, configurable pulse period | REQ-DAEMON-1 | a LiveAgent runs fully daemon-hosted (spawn→Psyche-loop→commune-ingest→pulse) with **no** separate listener/wrapper process; the relay is killable+respawnable without losing state; period is config-driven |
| B4 | **Daemon-authoritative liveness**: the brain's endpoint table + `info.json` `status` supersede `is_process_alive(info.pid)` for daemon-hosted perches; localized behind the single liveness resolver (`is_online`/registry stale-clean) so external/interim perches still probe | REQ-HAZARD-DAEMON-HOSTED-LIVENESS | a daemon-hosted Psyche reads alive/offline from the daemon `status`, not its pid; killing the summarizer subprocess does **not** flip the perch offline (regression test); external perches still pid-probe |
| B5 | **Idempotent broker↔brain boundary**: durable IDs + dedup-at-effect at every side-effect crossing (spool/PTY/registry), broker-owned recovery anchor; replay rules on brain restart | REQ-HAZARD-RESTART-IDEMPOTENT (Spike #6) | crash the brain before-intent / before-effect / after-effect at each boundary → exactly-once (no dup, no drop) across all three, value-set verified |
| B6 | **Auto-start + spt-hosted startup**: any `api` invocation starts the daemon if absent (`listen` anchor); spt-hosted startup = daemon spawn-session into a broker PTY then `api bind`, with an **in-memory** seed map replacing the `spt-store::seed` file bridge | REQ-DAEMON-3, REQ-START-3 | a cold `api` call transparently spins up the daemon; an spt-hosted session binds with **no seed file written** (the file path is dead); the harness-hosted seed is in-memory + consumed by `parent_pid` |
| B7 | **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 enables | REQ-DAEMON-4 (partial) | a supervised crash of a hosted session triggers the same echo-before-signoff + grace + stale-sweep ordering as graceful teardown (regression test) |
| B8 | **Digest daemon-half**: manifest `pty_digest` seam (adapter-supplied `input`/`agent`/`tool` patterns) → run the M3a `DigestParser` over the broker PTY; `spt digest <id>` snapshot pull + broker-pushed structured **delta-stream** on the R-TERM-3 substrate + opt-in Path-B persistence. Local addressing only | REQ-TERM-4 (`int`), ADR-0008 | `spt digest <id>` returns the live structured buffer for a daemon-hosted session; a subscriber receives only deltas; opt-in persistence appends to the history store; cross-node addressing explicitly deferred to M4 |
| B9 | **Activation sweep + E2E**: activate REQ-DAEMON-1/2/3/4 + REQ-START-3 + the 4 hazards + REQ-TERM-4(`int`); a daemon E2E (spawn→Psyche-loop→commune→**brain-restart-survives**→graceful-signoff) on the CI matrix; amend ROADMAP (M3b delivered) + CONTEXT; author the **M3c plan** stub | — | `check` green with M3b reqs activated; clippy `-D warnings`; CI matrix (ubuntu+windows) green; both-OS validated |

## M3b requirement-activation map

Activate as each task lands (default `["impl","unit"]`; daemon E2E supplies `int`):
- **REQ-DAEMON-2** (B0–B2), **REQ-HAZARD-HANDOFF-ARGV-COMPAT** (B0),
  **REQ-HAZARD-GEN-START-NOW** (B2), **REQ-DAEMON-1** (B3),
  **REQ-HAZARD-DAEMON-HOSTED-LIVENESS** (B4),
  **REQ-HAZARD-RESTART-IDEMPOTENT** (B5), **REQ-DAEMON-3** + **REQ-START-3** (B6),
  **REQ-DAEMON-4** (B7/B9 — the honor-all-hazards meta-gate),
  **REQ-TERM-4 `int`** (B8). The brain-restart-survives daemon E2E (B9) is the
  cross-cutting `int` evidence for REQ-DAEMON-1/2.

**Stay `[]` (M3c/M4):** all `REQ-UPD-*`, the M4 grill reqs (`REQ-INST-9..14`,
`REQ-PAIR-5/6/7`, `REQ-NOTIF-1/2`, `REQ-SEC-1`), cross-node digest addressing.

## Workspace change

Add `crates/spt-daemon` (above `spt-live`, below `spt`). Layering (R-ARCH-1, acyclic):

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

`spt-daemon` is **not** public SDK (R-ARCH-2 stays proto/runtime/msg) — it is the
internal supervisor; the `spt` binary becomes a thin CLI over it.

## Key seams (from the interim model M3b replaces)

| Concern | Interim locus | M3b move |
|---|---|---|
| listen relay loop | `spt/src/api/startup.rs` (`cmd_listen`) | daemon brain loop |
| pulse driver | `spt-live/src/pulse.rs` (`tick`, 5s constant) | daemon-scheduled, configurable |
| liveness | `spt-store/src/proc.rs` (`is_process_alive`) via `deliver::is_online` + `registry::clean_stale_entries` | daemon `status` behind the one resolver |
| seed bridge | `spt-store/src/seed.rs` (file by `parent_pid`) | in-memory daemon seed map |
| Psyche/echo/signoff | `spt-live` seams (already pure fns) | brain calls the same fns |
| broker PTY | `spt-term::PtySession` (M3a) | broker holds it, serves over IPC |

## Risks carried into M3b

- **The two-process IPC is the hard part.** Abstract the OS split (named pipe vs
  UDS) behind one transport trait from B0, exactly as M3a abstracted ConPTY vs
  forkpty behind `SessionSurface`. Version the frame schema from the first byte —
  retrofitting forward-compat after B3 is expensive.
- **Broker scope creep.** The broker holds *only* the ADR-0004 §B resources and
  **no logic**. A task that finds itself putting routing/registry/parse in the
  broker has crossed the line — stop and surface it. (The §B table grew to include
  Iroh streams; that is M4, not M3b.)
- **Gapless handoff (B2) is the milestone's load-bearing proof.** Spike #1 proved
  the shape on ConPTY; B2 makes it real in the daemon on **both** OSes. Treat the
  brain-restart regression as a gate, not polish.
- **Idempotency (B5) must thread durable IDs through every effect.** Design against
  Spike #6's two constraints (broker-owned recovery anchor; dedup-at-effect keyed by
  durable ID), not bolted on after.
- **Digest scope (B8).** Local addressing only; the `[subnet:]id@node` qualified
  form + cross-node fetch are M4. Resist pulling multi-subnet forward.


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

# M3b Plan — `spt-daemon` broker/brain process

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a-PLAN.md`. The
> ordered task layer between `ROADMAP.md`, `M3-PLAN.md` (the M3 umbrella), and
> `traceable-reqs.toml` (the requirement checklist), scoped to **M3b only**.
> Branch: `dev-freeform`. Authoritative architecture: ADR-0004 (broker/brain split
> + the §B ownership table + the closed §E spike gaps) and `M3-PLAN.md` §M3b.

> **Upstream is done:** Phase 0 spike-gate ✅ (Spikes #3/#4/#5/#6 all PASS — ADR-0004
> §E closed on both OSes) and **M3a ✅ delivered** — `spt-term` provides the
> single-surface PTY *mechanism* (SessionSurface + ConPTY/forkpty backend + DSR
> drain + injection + bounded byte-stream + digest parser primitive). **M3b builds
> the *process* that hosts many of those surfaces** behind a versioned local IPC,
> consolidating the interim no-daemon model.

## Goal

Replace the interim no-daemon model (M1/M2a/M2b: a long-lived `spt api listen`
process holds the perch + drives a relay/pulse loop; a seed *file* bridges
binding; per-pid `is_process_alive` is liveness) with the **real architecture**
(ADR-0004): one per-machine `spt-daemon`, split into a **broker** (stable kernel
holding the un-transferable resources) and a **brain** (restartable logic), so a
routine self-update swaps the brain with **zero endpoint interruption**.

**M3b done =** the daemon hosts the M2b LiveAgent lifecycle as in-process loops
(Psyche-as-loop, durable pulse — no separate listener/wrapper process, only a thin
stateless in-session relay residue) · the broker hosts `spt-term` PTYs + harness
children + sockets behind a **versioned local IPC** that a newer brain speaks to an
older broker · **a brain kill/restart leaves the child + its live output stream
intact and gapless** (Spike #1 shape, made real) · liveness for daemon-hosted
perches is the daemon's `status`, never `is_process_alive(info.pid)` · every
broker↔brain side-effect boundary is **exactly-once** across a brain crash (Spike
#6) · any `api` invocation **auto-starts** the daemon · spt-hosted startup is
spawn-session→`api bind` with an **in-memory** seed (no file) · the **digest
daemon-half** completes REQ-TERM-4 (`int`) · `cargo test --workspace` + clippy
`-D warnings` + `traceable-reqs check` green on the CI matrix (ubuntu + windows).

## Scope

### In — the `spt-daemon` crate (above `spt-live`, below `spt`)

- **broker** (stable kernel, `REQ-DAEMON-2`) — owns **only** the resources a live
  consumer would lose continuity on if the brain restarted (ADR-0004 §B normative
  table): PTY master handles (**from `spt-term::PtySession`**), spawned harness
  child processes, accepted local client sockets, listening sockets. No logic.
  Almost never restarts.
- **brain** (userspace, `REQ-DAEMON-2`) — all logic: routing, registry, manifest
  parse, **the re-hosted M2b lifecycle**. Restarts freely; rehydrates from disk +
  re-attaches to broker-held handles over IPC. `gen_start = now()` on cold-start
  **and** handoff (`REQ-HAZARD-GEN-START-NOW` / 2.4).
- **versioned local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3) — named pipe
  (Windows) / Unix-domain socket, behind one transport trait (mirrors how
  `SessionSurface` abstracts the PTY OS-split). A version handshake + a
  forward-compatible frame schema so a **newer brain talks to an older broker**.
- **consolidation** (`REQ-DAEMON-1`) — the `spt api listen` poll-loop + the pulse
  loop + the Psyche loop move **into the brain** as scheduled in-process loops; the
  only residue is the thin **stateless in-session relay** for harness-hosted
  sessions (topology 1), freely killable. Durable, configurable pulse period
  replacing the interim 5s constant.
- **daemon-authoritative liveness** (`REQ-HAZARD-DAEMON-HOSTED-LIVENESS` / 2.5) —
  the daemon's in-memory endpoint table + a `status` field on `info.json`
  supersede `is_process_alive(info.pid)` for **daemon-hosted** perches; localized
  behind the single liveness resolver so external/interim perches still probe.
- **idempotent broker↔brain boundary** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2,
  Spike #6) — durable IDs + dedup-at-effect at every side-effect crossing (spool
  write, PTY write, registry update), with a **broker-owned recovery anchor**;
  exactly-once across a brain crash at before-intent / before-effect / after-effect.
- **auto-start** (`REQ-DAEMON-3`) — any `api` invocation starts the daemon if
  absent (`listen` is the reliable anchor); ensure-running lives in the `api` layer.
- **spt-hosted startup** (`REQ-START-3`) — daemon spawn-session into a broker PTY,
  then `api bind`; **in-memory** seed map replaces the `spt-store::seed` file bridge.
- **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).
- **digest daemon-half** (`REQ-TERM-4` `int`, ADR-0008) — manifest-sourcing of the
  `input`/`agent`/`tool` patterns (the `pty_digest` manifest seam), running the
  M3a `DigestParser` over the broker PTY; `spt digest <id>` snapshot pull, the
  broker-pushed structured **delta-stream** on the R-TERM-3 substrate, and opt-in
  Path-B persistence. Local addressing only (the qualified `[subnet:]id@node` form
  is M4); `REQ-DAEMON-4` (honor every KNOWN-HAZARDS invariant) is the meta-gate.

### Out

- networking / P2P, multi-subnet, notifications, **cross-node** digest addressing —
  **M4** (`spt-net`). The QUIC-stream broker-ownership *implementation* is M4
  (only its *shape* was spiked, #3).
- self-update (update-class taxonomy, signing, ripple) — **M3c**.
- whole-daemon live FD-passing (zero-interruption even for *broker* updates) —
  explicit future polish, not v1 (ADR-0004 consequences).

## Clean-room posture

- **`spt-daemon` is brand-new.** The sister ran poll listeners + Psyche wrappers as
  separate processes; ADR-0004 deliberately consolidates them. Clean-room the crate.
- **Re-host, don't rewrite.** The M2b lifecycle is already pure, unit-tested
  functions in `spt-live` (`spawn_psyche`, `resume_psyche`, `run_echo_commune`,
  `ingest_drops`, `graceful_signoff`, `sweep_stale_signoff`, `pulse::tick`,
  `fetch_history`, `write_resume_commune`, `compose_psyche_prompt`). The brain
  *calls* these as loops; the ordering invariants (3.3 / 1.1 / 3.2) stay where they
  are. **Reuse `spt-term::PtySession` for the broker's PTYs** — do not re-implement.
- **Spike findings are binding inputs:** #1 (no-EOF-while-writer-held + DSR; the
  broker-survives-brain-restart shape), #3 (live-stream survival shape), #6
  (broker-owned recovery anchor + dedup-at-effect keyed by durable ID).

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

**Assessment — likely none.** The M3b reqs already exist: `REQ-DAEMON-1/2/3/4`,
`REQ-START-3`, `REQ-HAZARD-HANDOFF-ARGV-COMPAT`, `REQ-HAZARD-GEN-START-NOW`,
`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`, `REQ-HAZARD-RESTART-IDEMPOTENT`, and
`REQ-TERM-4` (`int` stage). They sit `required_stages = []`; activate as each task
lands. **If** the versioned-IPC frame contract or the in-session relay turns out to
need its own conformance id during B0/B3, register it in `traceable-reqs.toml`
first, then satisfy it.

## Sequencing rationale

Scaffold + the **versioned IPC transport** (the load-bearing seam everything rides)
→ the **broker kernel** (owns resources, holds no logic) → the **brain + gapless
handoff** (the milestone's hardest proof: kill brain, child + stream survive) →
**consolidation** (fold the interim loops in) → **daemon liveness** (localized swap)
→ **idempotent boundary** (durable IDs at every effect) → **auto-start + in-memory
startup** (retire the seed file) → **orphan-watch** → **digest daemon-half** →
activation + E2E. Each task tags evidence + activates its reqs in the same commit;
`traceable-reqs check` green and **both OSes** validated before the next (the M3a
discipline: Windows local + Linux via gravity-linux).

## Tasks — `spt-daemon` (the broker/brain process)

| # | Task | Reqs / hazards | Acceptance |
|---|------|----------------|------------|
| B0 | **Scaffold `crates/spt-daemon`** (above spt-live, below spt; deps spt-store/msg/runtime/term/live) + the **versioned local-IPC transport**: a `DaemonTransport` trait over named-pipe (win) / UDS (unix), a length-framed message codec, and a **version handshake** with a forward-compatible frame schema | REQ-DAEMON-2 (foundation), REQ-HAZARD-HANDOFF-ARGV-COMPAT | crate compiles; layering `…→spt-live→spt-daemon→spt` acyclic; a brain frame with an unknown trailing field is accepted by an older broker (forward-compat unit test); `check` green |
| B1 | **Broker kernel**: spawn a child under a `spt-term::PtySession`; hold the PTY master + child + a listening/client socket; serve them over IPC (subscribe-to-output, write-input, resize, lifecycle). **No logic in the broker.** | REQ-DAEMON-2 (broker half) | a broker hosts one real PTY child on both OSes, streams its output + accepts injected input over IPC; the ADR-0004 §B ownership table is the only state it holds (review-asserted) |
| B2 | **Brain + gapless handoff**: brain connects over the versioned IPC, subscribes to the broker's PTY output, sends input; **a brain kill + restart re-attaches with no lost/dup output and the child untouched** (Spike #1 made real). `gen_start = now()` on cold-start **and** handoff | REQ-HAZARD-GEN-START-NOW, REQ-DAEMON-2 (brain half) | regression test: spawn child → brain attached → kill+restart brain ≥10× → child alive throughout, output stream contiguous across each restart, `gen_start` advances each handoff |
| B3 | **Consolidation**: fold the `api listen` poll-loop + pulse + Psyche loops into the brain as scheduled in-process loops (re-hosting the spt-live seams); leave only the thin **stateless in-session relay** for harness-hosted sessions. Durable, configurable pulse period | REQ-DAEMON-1 | a LiveAgent runs fully daemon-hosted (spawn→Psyche-loop→commune-ingest→pulse) with **no** separate listener/wrapper process; the relay is killable+respawnable without losing state; period is config-driven |
| B4 | **Daemon-authoritative liveness**: the brain's endpoint table + `info.json` `status` supersede `is_process_alive(info.pid)` for daemon-hosted perches; localized behind the single liveness resolver (`is_online`/registry stale-clean) so external/interim perches still probe | REQ-HAZARD-DAEMON-HOSTED-LIVENESS | a daemon-hosted Psyche reads alive/offline from the daemon `status`, not its pid; killing the summarizer subprocess does **not** flip the perch offline (regression test); external perches still pid-probe |
| B5 | **Idempotent broker↔brain boundary**: durable IDs + dedup-at-effect at every side-effect crossing (spool/PTY/registry), broker-owned recovery anchor; replay rules on brain restart | REQ-HAZARD-RESTART-IDEMPOTENT (Spike #6) | crash the brain before-intent / before-effect / after-effect at each boundary → exactly-once (no dup, no drop) across all three, value-set verified |
| B6 | **Auto-start + spt-hosted startup**: any `api` invocation starts the daemon if absent (`listen` anchor); spt-hosted startup = daemon spawn-session into a broker PTY then `api bind`, with an **in-memory** seed map replacing the `spt-store::seed` file bridge | REQ-DAEMON-3, REQ-START-3 | a cold `api` call transparently spins up the daemon; an spt-hosted session binds with **no seed file written** (the file path is dead); the harness-hosted seed is in-memory + consumed by `parent_pid` |
| B7 | **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 enables | REQ-DAEMON-4 (partial) | a supervised crash of a hosted session triggers the same echo-before-signoff + grace + stale-sweep ordering as graceful teardown (regression test) |
| B8 | **Digest daemon-half**: manifest `pty_digest` seam (adapter-supplied `input`/`agent`/`tool` patterns) → run the M3a `DigestParser` over the broker PTY; `spt digest <id>` snapshot pull + broker-pushed structured **delta-stream** on the R-TERM-3 substrate + opt-in Path-B persistence. Local addressing only | REQ-TERM-4 (`int`), ADR-0008 | `spt digest <id>` returns the live structured buffer for a daemon-hosted session; a subscriber receives only deltas; opt-in persistence appends to the history store; cross-node addressing explicitly deferred to M4 |
| B9 | **Activation sweep + E2E**: activate REQ-DAEMON-1/2/3/4 + REQ-START-3 + the 4 hazards + REQ-TERM-4(`int`); a daemon E2E (spawn→Psyche-loop→commune→**brain-restart-survives**→graceful-signoff) on the CI matrix; amend ROADMAP (M3b delivered) + CONTEXT; author the **M3c plan** stub | — | `check` green with M3b reqs activated; clippy `-D warnings`; CI matrix (ubuntu+windows) green; both-OS validated |

## M3b requirement-activation map

Activate as each task lands (default `["impl","unit"]`; daemon E2E supplies `int`):
- **REQ-DAEMON-2** (B0–B2), **REQ-HAZARD-HANDOFF-ARGV-COMPAT** (B0),
  **REQ-HAZARD-GEN-START-NOW** (B2), **REQ-DAEMON-1** (B3),
  **REQ-HAZARD-DAEMON-HOSTED-LIVENESS** (B4),
  **REQ-HAZARD-RESTART-IDEMPOTENT** (B5), **REQ-DAEMON-3** + **REQ-START-3** (B6),
  **REQ-DAEMON-4** (B7/B9 — the honor-all-hazards meta-gate),
  **REQ-TERM-4 `int`** (B8). The brain-restart-survives daemon E2E (B9) is the
  cross-cutting `int` evidence for REQ-DAEMON-1/2.

**Stay `[]` (M3c/M4):** all `REQ-UPD-*`, the M4 grill reqs (`REQ-INST-9..14`,
`REQ-PAIR-5/6/7`, `REQ-NOTIF-1/2`, `REQ-SEC-1`), cross-node digest addressing.

## Workspace change

Add `crates/spt-daemon` (above `spt-live`, below `spt`). Layering (R-ARCH-1, acyclic):

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

`spt-daemon` is **not** public SDK (R-ARCH-2 stays proto/runtime/msg) — it is the
internal supervisor; the `spt` binary becomes a thin CLI over it.

## Key seams (from the interim model M3b replaces)

| Concern | Interim locus | M3b move |
|---|---|---|
| listen relay loop | `spt/src/api/startup.rs` (`cmd_listen`) | daemon brain loop |
| pulse driver | `spt-live/src/pulse.rs` (`tick`, 5s constant) | daemon-scheduled, configurable |
| liveness | `spt-store/src/proc.rs` (`is_process_alive`) via `deliver::is_online` + `registry::clean_stale_entries` | daemon `status` behind the one resolver |
| seed bridge | `spt-store/src/seed.rs` (file by `parent_pid`) | in-memory daemon seed map |
| Psyche/echo/signoff | `spt-live` seams (already pure fns) | brain calls the same fns |
| broker PTY | `spt-term::PtySession` (M3a) | broker holds it, serves over IPC |

## Risks carried into M3b

- **The two-process IPC is the hard part.** Abstract the OS split (named pipe vs
  UDS) behind one transport trait from B0, exactly as M3a abstracted ConPTY vs
  forkpty behind `SessionSurface`. Version the frame schema from the first byte —
  retrofitting forward-compat after B3 is expensive.
- **Broker scope creep.** The broker holds *only* the ADR-0004 §B resources and
  **no logic**. A task that finds itself putting routing/registry/parse in the
  broker has crossed the line — stop and surface it. (The §B table grew to include
  Iroh streams; that is M4, not M3b.)
- **Gapless handoff (B2) is the milestone's load-bearing proof.** Spike #1 proved
  the shape on ConPTY; B2 makes it real in the daemon on **both** OSes. Treat the
  brain-restart regression as a gate, not polish.
- **Idempotency (B5) must thread durable IDs through every effect.** Design against
  Spike #6's two constraints (broker-owned recovery anchor; dedup-at-effect keyed by
  durable ID), not bolted on after.
- **Digest scope (B8).** Local addressing only; the `[subnet:]id@node` qualified
  form + cross-node fetch are M4. Resist pulling multi-subnet forward.

-# M3b Plan — `spt-daemon` broker/brain process (stub)

+# M3b Plan — `spt-daemon` broker/brain process

> ordered task layer between `ROADMAP.md`, `M3-PLAN.md` (the M3 umbrella), and

-> `traceable-reqs.toml`, scoped to **M3b only**. Branch: `dev-freeform`. This is a

-> **stub** — the full task table is authored when M3b starts (after M3a, now

-> delivered). Authoritative scope lives in [`M3-PLAN.md`](./M3-PLAN.md) §M3b.

+> `traceable-reqs.toml` (the requirement checklist), scoped to **M3b only**.

+> Branch: `dev-freeform`. Authoritative architecture: ADR-0004 (broker/brain split

+> + the §B ownership table + the closed §E spike gaps) and `M3-PLAN.md` §M3b.

-> **Upstream is done:** Phase 0 spike-gate ✅ (Spikes #3/#4/#5/#6 PASS) and **M3a

-> ✅ delivered** — `spt-term` provides the single-surface PTY *mechanism*

-> (SessionSurface + ConPTY/forkpty backend + DSR drain + injection + bounded

-> byte-stream + digest parser primitive). **M3b builds the *process* that hosts

-> many of those surfaces** behind versioned IPC.

+> **Upstream is done:** Phase 0 spike-gate ✅ (Spikes #3/#4/#5/#6 all PASS — ADR-0004

+> §E closed on both OSes) and **M3a ✅ delivered** — `spt-term` provides the

+> single-surface PTY *mechanism* (SessionSurface + ConPTY/forkpty backend + DSR

+> drain + injection + bounded byte-stream + digest parser primitive). **M3b builds

+> the *process* that hosts many of those surfaces** behind a versioned local IPC,

+> consolidating the interim no-daemon model.

-Replace the interim no-daemon model (M1/M2a/M2b: the `api listen` process holds

-the perch + relays, seed bridges via a file) with the **real architecture**

-(ADR-0004): one per-machine `spt-daemon` with a **broker/brain split** that owns

-all per-machine state and re-hosts the M2b lifecycle as in-process loops with

-**daemon-authoritative liveness**, hosting `spt-term` surfaces behind a versioned

-local IPC, with an **idempotent** broker↔brain boundary and **auto-start**.

+Replace the interim no-daemon model (M1/M2a/M2b: a long-lived `spt api listen`

+process holds the perch + drives a relay/pulse loop; a seed *file* bridges

+binding; per-pid `is_process_alive` is liveness) with the **real architecture**

+(ADR-0004): one per-machine `spt-daemon`, split into a **broker** (stable kernel

+holding the un-transferable resources) and a **brain** (restartable logic), so a

+routine self-update swaps the brain with **zero endpoint interruption**.

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

-interim glue); the broker hosts `spt-term` PTYs behind versioned IPC; liveness is

-the daemon's `status` field (not `is_process_alive(pid)`); every broker↔brain

-side-effect boundary is idempotent (Spike #6); any `api` call auto-starts the

-daemon; `cargo test --workspace` + clippy `-D warnings` + `traceable-reqs check`

+**M3b done =** the daemon hosts the M2b LiveAgent lifecycle as in-process loops

+(Psyche-as-loop, durable pulse — no separate listener/wrapper process, only a thin

+stateless in-session relay residue) · the broker hosts `spt-term` PTYs + harness

+children + sockets behind a **versioned local IPC** that a newer brain speaks to an

+older broker · **a brain kill/restart leaves the child + its live output stream

+intact and gapless** (Spike #1 shape, made real) · liveness for daemon-hosted

+perches is the daemon's `status`, never `is_process_alive(info.pid)` · every

+broker↔brain side-effect boundary is **exactly-once** across a brain crash (Spike

+#6) · any `api` invocation **auto-starts** the daemon · spt-hosted startup is

+spawn-session→`api bind` with an **in-memory** seed (no file) · the **digest

+daemon-half** completes REQ-TERM-4 (`int`) · `cargo test --workspace` + clippy

+`-D warnings` + `traceable-reqs check` green on the CI matrix (ubuntu + windows).

-## Scope (authoritative detail: `M3-PLAN.md` §M3b)

### In — the `spt-daemon` crate (above `spt-live`, below `spt`)

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

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

-  children, accepted local client sockets, listening sockets. Minimal **versioned

-  local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3).

-- **brain** (userspace) — routing, registry, the re-hosted M2b lifecycle

-  (Psyche-as-loop + durable pulse period), manifest parse. Restarts freely;

-  rehydrates from disk + 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; only the thin stateless in-session relay remains (topology 1).

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

-- **idempotent delivery** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2 — Spike #6).

-- **auto-start** (`REQ-DAEMON-3`) + **spt-hosted startup** (`REQ-START-3`: spawn →

-  `api bind`, in-memory seed, no seed file) + **orphan-watch** (carry the M2b

-  grace 1.1 / echo 3.3 / stale-sweep 3.2 ordering into the supervised-crash path).

-- **digest daemon-half** — the M3b completion of REQ-TERM-4: manifest-sourcing of

-  the `input/agent/tool` patterns, `spt digest <[subnet:]id[@node]>` snapshot

-  pull, the broker-pushed structured **delta-stream** (riding the R-TERM-3

-  byte-stream substrate), and opt-in Path-B persistence. **Activates REQ-TERM-4

-  `int`** over the M3a parser primitive.

+- **broker** (stable kernel, `REQ-DAEMON-2`) — owns **only** the resources a live

+  consumer would lose continuity on if the brain restarted (ADR-0004 §B normative

+  table): PTY master handles (**from `spt-term::PtySession`**), spawned harness

+  child processes, accepted local client sockets, listening sockets. No logic.

+- **brain** (userspace, `REQ-DAEMON-2`) — all logic: routing, registry, manifest

+  parse, **the re-hosted M2b lifecycle**. Restarts freely; rehydrates from disk +

+  re-attaches to broker-held handles over IPC. `gen_start = now()` on cold-start

+  **and** handoff (`REQ-HAZARD-GEN-START-NOW` / 2.4).

+- **versioned local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3) — named pipe

+  (Windows) / Unix-domain socket, behind one transport trait (mirrors how

+  `SessionSurface` abstracts the PTY OS-split). A version handshake + a

+  forward-compatible frame schema so a **newer brain talks to an older broker**.

+- **consolidation** (`REQ-DAEMON-1`) — the `spt api listen` poll-loop + the pulse

+  loop + the Psyche loop move **into the brain** as scheduled in-process loops; the

+  only residue is the thin **stateless in-session relay** for harness-hosted

+  sessions (topology 1), freely killable. Durable, configurable pulse period

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

+  the daemon's in-memory endpoint table + a `status` field on `info.json`

+  supersede `is_process_alive(info.pid)` for **daemon-hosted** perches; localized

+  behind the single liveness resolver so external/interim perches still probe.

+- **idempotent broker↔brain boundary** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2,

+  Spike #6) — durable IDs + dedup-at-effect at every side-effect crossing (spool

+  write, PTY write, registry update), with a **broker-owned recovery anchor**;

+  exactly-once across a brain crash at before-intent / before-effect / after-effect.

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

+  absent (`listen` is the reliable anchor); ensure-running lives in the `api` layer.

+- **spt-hosted startup** (`REQ-START-3`) — daemon spawn-session into a broker PTY,

+  then `api bind`; **in-memory** seed map replaces the `spt-store::seed` file bridge.

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

+- **digest daemon-half** (`REQ-TERM-4` `int`, ADR-0008) — manifest-sourcing of the

+  `input`/`agent`/`tool` patterns (the `pty_digest` manifest seam), running the

+  M3a `DigestParser` over the broker PTY; `spt digest <id>` snapshot pull, the

+  broker-pushed structured **delta-stream** on the R-TERM-3 substrate, and opt-in

+  Path-B persistence. Local addressing only (the qualified `[subnet:]id@node` form

+  is M4); `REQ-DAEMON-4` (honor every KNOWN-HAZARDS invariant) is the meta-gate.

-- networking / P2P + multi-subnet + notifications (M4); the QUIC-stream broker

-  ownership *implementation* (M4 — shape validated by Spike #3); self-update (M3c).

+- networking / P2P, multi-subnet, notifications, **cross-node** digest addressing —

+  **M4** (`spt-net`). The QUIC-stream broker-ownership *implementation* is M4

+- self-update (update-class taxonomy, signing, ripple) — **M3c**.

+- whole-daemon live FD-passing (zero-interruption even for *broker* updates) —

+  explicit future polish, not v1 (ADR-0004 consequences).

-## Requirements to activate (currently `required_stages = []`)

-`REQ-HAZARD-GEN-START-NOW`, and **REQ-TERM-4 `int`** (the digest daemon-half).

-Activate each as its task lands (TRACEABILITY rule 5). New requirements →

-register in `traceable-reqs.toml` first (rule 3).

+- **`spt-daemon` is brand-new.** The sister ran poll listeners + Psyche wrappers as

+  separate processes; ADR-0004 deliberately consolidates them. Clean-room the crate.

+- **Re-host, don't rewrite.** The M2b lifecycle is already pure, unit-tested

+  functions in `spt-live` (`spawn_psyche`, `resume_psyche`, `run_echo_commune`,

+  `ingest_drops`, `graceful_signoff`, `sweep_stale_signoff`, `pulse::tick`,

+  `fetch_history`, `write_resume_commune`, `compose_psyche_prompt`). The brain

+  *calls* these as loops; the ordering invariants (3.3 / 1.1 / 3.2) stay where they

+  are. **Reuse `spt-term::PtySession` for the broker's PTYs** — do not re-implement.

+- **Spike findings are binding inputs:** #1 (no-EOF-while-writer-held + DSR; the

+  broker-survives-brain-restart shape), #3 (live-stream survival shape), #6

+  (broker-owned recovery anchor + dedup-at-effect keyed by durable ID).

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

+**Assessment — likely none.** The M3b reqs already exist: `REQ-DAEMON-1/2/3/4`,

+`REQ-TERM-4` (`int` stage). They sit `required_stages = []`; activate as each task

+lands. **If** the versioned-IPC frame contract or the in-session relay turns out to

+need its own conformance id during B0/B3, register it in `traceable-reqs.toml`

+Scaffold + the **versioned IPC transport** (the load-bearing seam everything rides)

+→ the **broker kernel** (owns resources, holds no logic) → the **brain + gapless

+handoff** (the milestone's hardest proof: kill brain, child + stream survive) →

+**consolidation** (fold the interim loops in) → **daemon liveness** (localized swap)

+→ **idempotent boundary** (durable IDs at every effect) → **auto-start + in-memory

+startup** (retire the seed file) → **orphan-watch** → **digest daemon-half** →

+activation + E2E. Each task tags evidence + activates its reqs in the same commit;

+`traceable-reqs check` green and **both OSes** validated before the next (the M3a

+discipline: Windows local + Linux via gravity-linux).

+## Tasks — `spt-daemon` (the broker/brain process)

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

+| B0 | **Scaffold `crates/spt-daemon`** (above spt-live, below spt; deps spt-store/msg/runtime/term/live) + the **versioned local-IPC transport**: a `DaemonTransport` trait over named-pipe (win) / UDS (unix), a length-framed message codec, and a **version handshake** with a forward-compatible frame schema | REQ-DAEMON-2 (foundation), REQ-HAZARD-HANDOFF-ARGV-COMPAT | crate compiles; layering `…→spt-live→spt-daemon→spt` acyclic; a brain frame with an unknown trailing field is accepted by an older broker (forward-compat unit test); `check` green |

+| B1 | **Broker kernel**: spawn a child under a `spt-term::PtySession`; hold the PTY master + child + a listening/client socket; serve them over IPC (subscribe-to-output, write-input, resize, lifecycle). **No logic in the broker.** | REQ-DAEMON-2 (broker half) | a broker hosts one real PTY child on both OSes, streams its output + accepts injected input over IPC; the ADR-0004 §B ownership table is the only state it holds (review-asserted) |

+| B2 | **Brain + gapless handoff**: brain connects over the versioned IPC, subscribes to the broker's PTY output, sends input; **a brain kill + restart re-attaches with no lost/dup output and the child untouched** (Spike #1 made real). `gen_start = now()` on cold-start **and** handoff | REQ-HAZARD-GEN-START-NOW, REQ-DAEMON-2 (brain half) | regression test: spawn child → brain attached → kill+restart brain ≥10× → child alive throughout, output stream contiguous across each restart, `gen_start` advances each handoff |

+| B3 | **Consolidation**: fold the `api listen` poll-loop + pulse + Psyche loops into the brain as scheduled in-process loops (re-hosting the spt-live seams); leave only the thin **stateless in-session relay** for harness-hosted sessions. Durable, configurable pulse period | REQ-DAEMON-1 | a LiveAgent runs fully daemon-hosted (spawn→Psyche-loop→commune-ingest→pulse) with **no** separate listener/wrapper process; the relay is killable+respawnable without losing state; period is config-driven |

+| B4 | **Daemon-authoritative liveness**: the brain's endpoint table + `info.json` `status` supersede `is_process_alive(info.pid)` for daemon-hosted perches; localized behind the single liveness resolver (`is_online`/registry stale-clean) so external/interim perches still probe | REQ-HAZARD-DAEMON-HOSTED-LIVENESS | a daemon-hosted Psyche reads alive/offline from the daemon `status`, not its pid; killing the summarizer subprocess does **not** flip the perch offline (regression test); external perches still pid-probe |

+| B5 | **Idempotent broker↔brain boundary**: durable IDs + dedup-at-effect at every side-effect crossing (spool/PTY/registry), broker-owned recovery anchor; replay rules on brain restart | REQ-HAZARD-RESTART-IDEMPOTENT (Spike #6) | crash the brain before-intent / before-effect / after-effect at each boundary → exactly-once (no dup, no drop) across all three, value-set verified |

+| B6 | **Auto-start + spt-hosted startup**: any `api` invocation starts the daemon if absent (`listen` anchor); spt-hosted startup = daemon spawn-session into a broker PTY then `api bind`, with an **in-memory** seed map replacing the `spt-store::seed` file bridge | REQ-DAEMON-3, REQ-START-3 | a cold `api` call transparently spins up the daemon; an spt-hosted session binds with **no seed file written** (the file path is dead); the harness-hosted seed is in-memory + consumed by `parent_pid` |

+| B7 | **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 enables | REQ-DAEMON-4 (partial) | a supervised crash of a hosted session triggers the same echo-before-signoff + grace + stale-sweep ordering as graceful teardown (regression test) |

+| B8 | **Digest daemon-half**: manifest `pty_digest` seam (adapter-supplied `input`/`agent`/`tool` patterns) → run the M3a `DigestParser` over the broker PTY; `spt digest <id>` snapshot pull + broker-pushed structured **delta-stream** on the R-TERM-3 substrate + opt-in Path-B persistence. Local addressing only | REQ-TERM-4 (`int`), ADR-0008 | `spt digest <id>` returns the live structured buffer for a daemon-hosted session; a subscriber receives only deltas; opt-in persistence appends to the history store; cross-node addressing explicitly deferred to M4 |

+| B9 | **Activation sweep + E2E**: activate REQ-DAEMON-1/2/3/4 + REQ-START-3 + the 4 hazards + REQ-TERM-4(`int`); a daemon E2E (spawn→Psyche-loop→commune→**brain-restart-survives**→graceful-signoff) on the CI matrix; amend ROADMAP (M3b delivered) + CONTEXT; author the **M3c plan** stub | — | `check` green with M3b reqs activated; clippy `-D warnings`; CI matrix (ubuntu+windows) green; both-OS validated |

+Activate as each task lands (default `["impl","unit"]`; daemon E2E supplies `int`):

+- **REQ-DAEMON-2** (B0–B2), **REQ-HAZARD-HANDOFF-ARGV-COMPAT** (B0),

+  **REQ-HAZARD-GEN-START-NOW** (B2), **REQ-DAEMON-1** (B3),

+  **REQ-HAZARD-DAEMON-HOSTED-LIVENESS** (B4),

+  **REQ-HAZARD-RESTART-IDEMPOTENT** (B5), **REQ-DAEMON-3** + **REQ-START-3** (B6),

+  **REQ-DAEMON-4** (B7/B9 — the honor-all-hazards meta-gate),

+  **REQ-TERM-4 `int`** (B8). The brain-restart-survives daemon E2E (B9) is the

+  cross-cutting `int` evidence for REQ-DAEMON-1/2.

+**Stay `[]` (M3c/M4):** all `REQ-UPD-*`, the M4 grill reqs (`REQ-INST-9..14`,

+`REQ-PAIR-5/6/7`, `REQ-NOTIF-1/2`, `REQ-SEC-1`), cross-node digest addressing.

-Add `crates/spt-daemon` (sixth-ish layer, above `spt-live`, below `spt`):

+Add `crates/spt-daemon` (above `spt-live`, below `spt`). Layering (R-ARCH-1, acyclic):

+`spt-daemon` is **not** public SDK (R-ARCH-2 stays proto/runtime/msg) — it is the

+internal supervisor; the `spt` binary becomes a thin CLI over it.

-- **Versioned IPC = newer brain ↔ older broker** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT`):

-  the broker is the stable kernel; never break its handoff contract.

-- **`spt-term` is single-surface per handle** — the daemon supplies the

-  multiplexed, name-addressable, attach/detach supervisor *around* it. Do not push

-  fleet/registry concerns back down into `spt-term`.

-- **OS stream-neutrality (Spike #4/#5)** continues to hold through the IPC: the

-  delta-stream is a structured overlay on the same OS-neutral byte stream.

+## Key seams (from the interim model M3b replaces)

+| listen relay loop | `spt/src/api/startup.rs` (`cmd_listen`) | daemon brain loop |

+| pulse driver | `spt-live/src/pulse.rs` (`tick`, 5s constant) | daemon-scheduled, configurable |

+| liveness | `spt-store/src/proc.rs` (`is_process_alive`) via `deliver::is_online` + `registry::clean_stale_entries` | daemon `status` behind the one resolver |

+| seed bridge | `spt-store/src/seed.rs` (file by `parent_pid`) | in-memory daemon seed map |

+| Psyche/echo/signoff | `spt-live` seams (already pure fns) | brain calls the same fns |

+| broker PTY | `spt-term::PtySession` (M3a) | broker holds it, serves over IPC |

+- **The two-process IPC is the hard part.** Abstract the OS split (named pipe vs

+  UDS) behind one transport trait from B0, exactly as M3a abstracted ConPTY vs

+  forkpty behind `SessionSurface`. Version the frame schema from the first byte —

+  retrofitting forward-compat after B3 is expensive.

+- **Broker scope creep.** The broker holds *only* the ADR-0004 §B resources and

+  **no logic**. A task that finds itself putting routing/registry/parse in the

+  broker has crossed the line — stop and surface it. (The §B table grew to include

+- **Gapless handoff (B2) is the milestone's load-bearing proof.** Spike #1 proved

+  the shape on ConPTY; B2 makes it real in the daemon on **both** OSes. Treat the

+  brain-restart regression as a gate, not polish.

+- **Idempotency (B5) must thread durable IDs through every effect.** Design against

+  Spike #6's two constraints (broker-owned recovery anchor; dedup-at-effect keyed by

+- **Digest scope (B8).** Local addressing only; the `[subnet:]id@node` qualified

+  form + cross-node fetch are M4. Resist pulling multi-subnet forward.

# M3b Plan — `spt-daemon` broker/brain process (stub)

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a-PLAN.md`. The
> ordered task layer between `ROADMAP.md`, `M3-PLAN.md` (the M3 umbrella), and
> `traceable-reqs.toml`, scoped to **M3b only**. Branch: `dev-freeform`. This is a
> **stub** — the full task table is authored when M3b starts (after M3a, now
> delivered). Authoritative scope lives in [`M3-PLAN.md`](./M3-PLAN.md) §M3b.

> **Upstream is done:** Phase 0 spike-gate ✅ (Spikes #3/#4/#5/#6 PASS) and **M3a
> ✅ delivered** — `spt-term` provides the single-surface PTY *mechanism*
> (SessionSurface + ConPTY/forkpty backend + DSR drain + injection + bounded
> byte-stream + digest parser primitive). **M3b builds the *process* that hosts
> many of those surfaces** behind versioned IPC.

## Goal

Replace the interim no-daemon model (M1/M2a/M2b: the `api listen` process holds
the perch + relays, seed bridges via a file) with the **real architecture**
(ADR-0004): one per-machine `spt-daemon` with a **broker/brain split** that owns
all per-machine state and re-hosts the M2b lifecycle as in-process loops with
**daemon-authoritative liveness**, hosting `spt-term` surfaces behind a versioned
local IPC, with an **idempotent** broker↔brain boundary and **auto-start**.

**M3b done =** the daemon hosts the LiveAgent lifecycle (Psyche-as-loop, no
interim glue); the broker hosts `spt-term` PTYs behind versioned IPC; liveness is
the daemon's `status` field (not `is_process_alive(pid)`); every broker↔brain
side-effect boundary is idempotent (Spike #6); any `api` call auto-starts the
daemon; `cargo test --workspace` + clippy `-D warnings` + `traceable-reqs check`
green on the CI matrix.

## Scope (authoritative detail: `M3-PLAN.md` §M3b)

### In — the `spt-daemon` crate (above `spt-live`, below `spt`)

- **broker** (stable kernel) — owns only un-transferable resources per the
  ADR-0004 §B table: PTY master handles (**from `spt-term`**), spawned harness
  children, accepted local client sockets, listening sockets. Minimal **versioned
  local IPC** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT` / 2.3).
- **brain** (userspace) — routing, registry, the re-hosted M2b lifecycle
  (Psyche-as-loop + durable pulse period), manifest parse. Restarts freely;
  rehydrates from disk + 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; only the thin stateless in-session relay remains (topology 1).
- **daemon-authoritative liveness** (`REQ-HAZARD-DAEMON-HOSTED-LIVENESS` / 2.5).
- **idempotent delivery** (`REQ-HAZARD-RESTART-IDEMPOTENT` / 7.2 — Spike #6).
- **auto-start** (`REQ-DAEMON-3`) + **spt-hosted startup** (`REQ-START-3`: spawn →
  `api bind`, in-memory seed, no seed file) + **orphan-watch** (carry the M2b
  grace 1.1 / echo 3.3 / stale-sweep 3.2 ordering into the supervised-crash path).
- **digest daemon-half** — the M3b completion of REQ-TERM-4: manifest-sourcing of
  the `input/agent/tool` patterns, `spt digest <[subnet:]id[@node]>` snapshot
  pull, the broker-pushed structured **delta-stream** (riding the R-TERM-3
  byte-stream substrate), and opt-in Path-B persistence. **Activates REQ-TERM-4
  `int`** over the M3a parser primitive.

### Out

- networking / P2P + multi-subnet + notifications (M4); the QUIC-stream broker
  ownership *implementation* (M4 — shape validated by Spike #3); self-update (M3c).

## Requirements to activate (currently `required_stages = []`)

`REQ-DAEMON-1/3`, `REQ-START-3`, `REQ-HAZARD-DAEMON-HOSTED-LIVENESS`,
`REQ-HAZARD-RESTART-IDEMPOTENT`, `REQ-HAZARD-HANDOFF-ARGV-COMPAT`,
`REQ-HAZARD-GEN-START-NOW`, and **REQ-TERM-4 `int`** (the digest daemon-half).
Activate each as its task lands (TRACEABILITY rule 5). New requirements →
register in `traceable-reqs.toml` first (rule 3).

## Workspace change

Add `crates/spt-daemon` (sixth-ish layer, above `spt-live`, below `spt`):

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

## Carried constraints

- **Versioned IPC = newer brain ↔ older broker** (`REQ-HAZARD-HANDOFF-ARGV-COMPAT`):
  the broker is the stable kernel; never break its handoff contract.
- **`spt-term` is single-surface per handle** — the daemon supplies the
  multiplexed, name-addressable, attach/detach supervisor *around* it. Do not push
  fleet/registry concerns back down into `spt-term`.
- **OS stream-neutrality (Spike #4/#5)** continues to hold through the IPC: the
  delta-stream is a structured overlay on the same OS-neutral byte stream.


`M3b-PLAN.md` written — full task table (B0–B9), replacing the stub. Stopping here for your review per the plan (no M3b code yet, plan not committed).

## M3b plan shape

**Core bet (ADR-0004):** two real processes — **broker** (stable kernel, holds PTYs/children/sockets, no logic) + **brain** (restartable logic) over a **versioned local IPC** — so a brain-only self-update swaps logic with zero endpoint interruption. Spike #1/#3/#5/#6 already proved the shape on both OSes; M3b makes it real.

**10 tasks, each OS-validated + committed atomically (M3a discipline):**

| | Task | Key req |
|--|------|---------|
| B0 | scaffold + **versioned IPC transport** (named-pipe/UDS behind one trait, forward-compat frames) | DAEMON-2, HANDOFF-ARGV-COMPAT |
| B1 | **broker kernel** — holds only ADR-0004 §B resources, serves over IPC | DAEMON-2 |
| B2 | **brain + gapless handoff** — kill brain ≥10×, child+stream survive; `gen_start=now()` | GEN-START-NOW |
| B3 | **consolidation** — listen/pulse/Psyche loops → brain; thin relay residue | DAEMON-1 |
| B4 | **daemon-authoritative liveness** — `status` supersedes `is_process_alive` | DAEMON-HOSTED-LIVENESS |
| B5 | **idempotent boundary** — durable-ID + dedup-at-effect (Spike #6) | RESTART-IDEMPOTENT |
| B6 | **auto-start + in-memory startup** — retire the seed *file* | DAEMON-3, START-3 |
| B7 | orphan-watch — M2b ordering into supervised-crash | DAEMON-4 |
| B8 | **digest daemon-half** — `spt digest` pull + delta-stream + persist | **TERM-4 `int`** |
| B9 | activation + brain-restart E2E + docs + M3c stub | — |

**Load-bearing risk flagged:** B2 (gapless handoff) is the milestone's proof-gate; B0 IPC versioning must be forward-compat from byte one; broker must hold zero logic.

---

Two open items for you:

1. **M3b plan** — review/adjust? Want me to run it through `/gsd-review` or `plan-eng-review`, or is it good to commit + start B0?
2. **CI traceability job** — still red (pre-existing): it can't download the `traceable-reqs` binary from the private cross-org `BigscreenVR/traceable-reqs` using CI's repo-scoped token. One-line `ci.yml` + a PAT secret with BigscreenVR access fixes it. Want me to wire that (you'd add the secret value), or leave it?


fork BigscreenVR/traceable-reqs privately under my own GH user account and point CI to the fork instead


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


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


Forking. But heads-up on a real gap first: **GH Actions' default `GITHUB_TOKEN` is scoped to only the workflow's repo** — it can't read *any* other private repo, even another one you own. So a private fork still needs a PAT secret for CI. Also **forks don't copy releases/binary assets** (only git history), so I must re-mirror the `v0.1.0` linux binary onto the fork. I'll do the fork + mirror + CI wiring; the PAT secret is the one step only you can complete.

Let me check the upstream asset name, then fork.


Bash

echo "=== upstream release assets ==="; gh release view v0.1.0 --repo BigscreenVR/traceable-reqs --json assets,tagName 2>&1 | head -20; echo "=== my account ==="; gh api user --jq .login 2>&1

Inspect upstream release assets + confirm account


HANDOFF: trampoline -> C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.24\owl.exe


Upstream has 3 assets (linux/macos/windows); CI needs `*linux-x86_64`. Account = SaberMage. Forking (you directed this).


gh repo fork BigscreenVR/traceable-reqs --clone=false 2>&1 | head; echo "=== verify fork ==="; sleep 3; gh repo view SaberMage/traceable-reqs --json name,visibility,isFork,parent 2>&1 | head

Fork traceable-reqs to SaberMage


failed to fork: HTTP 403: The repository exists, but forking is disabled. (https://api.github.com/repos/BigscreenVR/traceable-reqs/forks)
=== verify fork ===
GraphQL: Could not resolve to a Repository with the name 'SaberMage/traceable-reqs'. (repository)


Forking is **disabled** on `BigscreenVR/traceable-reqs` (private-repo fork policy) — `gh repo fork` 403s. So a true linked fork isn't possible. The fallback is a **mirror** (private copy under SaberMage). Check whether I can even clone the upstream source first.


cd /tmp && rm -rf tr-mirror && gh repo clone BigscreenVR/traceable-reqs tr-mirror -- --bare 2>&1 | tail -5; echo "exit=$?"; ls -la /tmp/tr-mirror 2>/dev/null | head -3; cd /tmp/tr-mirror 2>/dev/null && git log --oneline -1 2>&1 | head -1

Test clone access to upstream


Cloning into bare repository 'tr-mirror'...
exit=0
total 2572
drwxr-xr-x 1 decid 197609   0 Jun  2 03:39 .
drwxr-xr-x 1 decid 197609   0 Jun  2 03:39 ..
936f00b Quote SKILL.md description to fix YAML frontmatter (#7) (#11)


[Request interrupted by user]


we'll just do the bigscreenvr PAT. what are the steps i take?


Good call — PAT is the clean path. I'll wire `ci.yml` now; you do the token + secret. Let me update CI and clean up the test clone.


Edit

      - name: Install traceable-reqs
        run: |
          gh release download --repo BigscreenVR/traceable-reqs \
            --pattern '*linux-x86_64' --output traceable-reqs
          chmod +x traceable-reqs
        env:
          GH_TOKEN: ${{ github.token }}

      - name: Install traceable-reqs
        run: |
          gh release download --repo BigscreenVR/traceable-reqs \
            --pattern '*linux-x86_64' --output traceable-reqs
          chmod +x traceable-reqs
        env:
          # Cross-org private repo: the default repo-scoped GITHUB_TOKEN cannot
          # read BigscreenVR/traceable-reqs. Supply a PAT with read access to it
          # as the TRACEABLE_REQS_TOKEN repo secret. See README/CI notes.
          GH_TOKEN: ${{ secrets.TRACEABLE_REQS_TOKEN }}


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

      - name: Install traceable-reqs
        run: |
          gh release download --repo BigscreenVR/traceable-reqs \
            --pattern '*linux-x86_64' --output traceable-reqs
          chmod +x traceable-reqs
        env:
          # Cross-org private repo: the default repo-scoped GITHUB_TOKEN cannot
          # read BigscreenVR/traceable-reqs. Supply a PAT with read access to it
          # as the TRACEABLE_REQS_TOKEN repo secret. See README/CI notes.
          GH_TOKEN: ${{ secrets.TRACEABLE_REQS_TOKEN }}

name: ci

on:
  push:
    branches: [main, dev-freeform]
  pull_request:

# Cancel superseded runs on the same ref.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ---- Rust build + test + lint (cross-platform: exercises both the
  # cfg(windows) OpenProcess path and the cfg(unix) libc path in proc.rs) ----
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - name: Install Rust (stable + clippy)
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - name: Cache cargo
        uses: Swatinem/rust-cache@v2
      - name: Build
        run: cargo build --workspace --all-targets
      - name: Test
        run: cargo test --workspace
      - name: Clippy (deny warnings)
        run: cargo clippy --workspace --all-targets -- -D warnings

  # ---- Requirement-traceability hard gate (TRACEABILITY.md layer 1) ----
  traceability:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install traceable-reqs
        run: |
          gh release download --repo BigscreenVR/traceable-reqs \
            --pattern '*linux-x86_64' --output traceable-reqs
          chmod +x traceable-reqs
        env:
          GH_TOKEN: ${{ github.token }}
      - name: Coverage gate (hard — fails PR on uncovered activated req)
        run: ./traceable-reqs check --json
      - name: Quality audit (advisory)
        run: ./traceable-reqs lint || true

-          GH_TOKEN: ${{ github.token }}

+          # Cross-org private repo: the default repo-scoped GITHUB_TOKEN cannot

+          # read BigscreenVR/traceable-reqs. Supply a PAT with read access to it

+          # as the TRACEABLE_REQS_TOKEN repo secret. See README/CI notes.

+          GH_TOKEN: ${{ secrets.TRACEABLE_REQS_TOKEN }}


Bash

Remove test mirror clone


cleaned


`ci.yml` updated to use `secrets.TRACEABLE_REQS_TOKEN`. Now your steps:

## 1. Create the PAT (read access to `BigscreenVR/traceable-reqs`)

**Fine-grained (preferred, minimal scope):**
1. github.com → **Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token**
2. **Resource owner: `BigscreenVR`** (the org)
   - If the org isn't selectable or shows "approval required": a BigscreenVR **org owner** must enable fine-grained PATs (Org Settings → Personal access tokens) and/or approve the token. If that's blocked → use Classic below.
3. **Repository access → Only select repositories → `BigscreenVR/traceable-reqs`**
4. **Permissions → Repository → Contents: Read-only** (covers release-asset download)
5. Generate → copy the token (shown once)

**Classic (fallback if the org blocks fine-grained):**
- Settings → Developer settings → **Tokens (classic) → Generate new token (classic)**
- Scope: **`repo`** (classic can't scope to one repo — full repo read/write) → copy
- Caveat: works only because your `SaberMage` account already has read access to that BigscreenVR repo.

## 2. Add it as a repo secret on `SaberMage/spt-core`

UI: repo → **Settings → Secrets and variables → Actions → New repository secret** → Name `TRACEABLE_REQS_TOKEN`, paste the value.

Or CLI (run it yourself so the token never passes through me) — type in this session with the `!` prefix:
```
! gh secret set TRACEABLE_REQS_TOKEN --repo SaberMage/spt-core
```
(it prompts for the value).

## 3. I commit + push the `ci.yml` change

Then the traceability job re-runs with the PAT. Tell me when the secret's set and I'll commit/push and watch the run go green.

One note: the secret is **not** available to workflow runs from outside PRs/forks — fine here since CI runs on your own `dev-freeform` pushes.

Want me to commit the `ci.yml` change now (so it's staged for the next push), or hold until the secret's in?


<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/reload-plugins</command-name>
            <command-message>reload-plugins</command-message>
            <command-args></command-args>


<local-command-stdout>Reloaded: 6 plugins · 0 skills · 39 agents · 9 hooks · 1 plugin MCP server · 1 plugin LSP server</local-command-stdout>


<command-name>/reload-skills</command-name>
            <command-message>reload-skills</command-message>
            <command-args></command-args>


<local-command-stdout>Reloaded skills: 157 skills available (no changes)</local-command-stdout>


secret is set