# M2b Plan — Live-agent lifecycle (`spt-live`)

> **Just-in-time, lightweight** — same pattern as `M0-PLAN.md` / `M1-PLAN.md` /
> `M2a-PLAN.md`. The ordered task layer between ROADMAP.md (milestone sequence)
> and `traceable-reqs.toml` (requirement checklist), scoped to **M2b only**.
> Honors ROADMAP §"lightweight but structured, GSD too heavy". Branch:
> `dev-freeform`.

> **M2 is split** (decision 2026-06-01): **M2a = the harness contract** (landed
> 2026-06-01 — `spt-runtime` + the `api` surface + harness-hosted startup + mock
> adapter + contract E2E). **M2b = the live-agent / Psyche lifecycle** (this
> plan): the `spt-live` crate that *executes* the lifecycle seams M2a only
> declared and parsed.

## Goal

Make a **LiveAgent** real: a Self with a Psyche companion that the harness drives
through its full lifecycle — Psyche spawn/resume, the pulse heartbeat,
commune/signoff drop-file ingest, the echo-commune, and the resume seam — all
behind the M2a manifest contract, still **harness-agnostic** (zero Claude Code
conventions in-repo; the mock adapter gains the lifecycle templates).

M2a built the *inbound/outbound contract surface* and **parsed** the lifecycle
manifest sections (`[session.psyche_*]`, `[session.echo_commune]`,
`[session.signoff]`, `[history]`, commune/signoff watched dirs) without executing
them. M2b builds the executor:

- **spawn-psyche seam** (`REQ-SEAM-PSYCHE`) — fresh + resume Psyche templates,
  spawned as a **nested Psyche perch** (`<id>-psyche`) via `ManifestRuntime`.
- **history subsystem** (`REQ-SEAM-HISTORY`) — the three strategies (fetcher /
  locate-normalize / native) feeding the echo-commune + resume briefs.
- **resume-session seam** (`REQ-SEAM-RESUME`) — fresh-with-preload + continue-
  existing.
- **drop-file ingest** — watch the manifest dirs, ingest `<id>-commune.md` /
  `<id>-signoff.md`, **single-writer** (spt-core owns + deletes; the mind is
  read-only — KNOWN-HAZARDS 6.4), with the direct-write precedence guard (6.5).
- **the orphan/signoff ordering invariants** — echo-commune *before* INIT_SIGNOFF
  (3.3), grace recheck before composing signoff (1.1), stale-signoff sweep on
  start (3.2).

**M2b done =** `spt-live` compiles + the `api listen` runner hosts the lifecycle
· `cargo test --workspace` green · a **LiveAgent E2E** proves: spawn a LiveAgent
→ its Psyche spawns as a nested perch → a commune drop-file is ingested (and
deleted by spt-core) → a graceful signoff fires the echo-commune *before*
teardown — all harness-agnostic via the mock adapter · `traceable-reqs check`
green with M2b reqs *activated* · CI stays green.

## Scope

**In:** the `spt-live` crate — `SpawnPsyche` (fresh/resume templates +
spt-core-owned `$psyche_prompt`: timestamp + incoming event envelope;
recursion-guard env set on summarizer children); the **history subsystem** (all
three `[history]` strategies, the native Path-B store reusing T7's
`history-log`); the **echo-commune** seam (cheap-model template consuming the
configured history; project-primary / live-conservative provenance per
`docs/CONTEXT-MEMORY.md`); the **pulse** heartbeat (interim: a loop in the
`api listen` runner — period from the spt-core global config knob); **drop-file
ingest** (watch the manifest commune/signoff dirs; single-writer ownership;
ingest→delete; precedence marker on context writes); the **graceful signoff +
boundary Self-resume-commune** path (echo-before-signoff ordering; grace recheck;
stale-signoff sweep); the **resume-session seam** (fresh-with-preload +
continue-existing); the `api listen` wiring that hosts all of it; mock-adapter
lifecycle templates + a LiveAgent E2E.

**Out:**
- **The consolidated `spt-daemon` (M3):** M2b keeps M1/M2a's interim no-daemon
  model — the Psyche is a **real process** and the `api listen` process runs the
  pulse/ingest loops. M3 moves both into the daemon (Psyche-as-loop,
  daemon-authoritative liveness — KNOWN-HAZARDS 2.5 / `REQ-HAZARD-DAEMON-HOSTED-
  LIVENESS`, `REQ-DAEMON-*`).
- **Orphan *crash* detection / supervision (M3):** M2b implements the **graceful**
  teardown paths (`spt shutdown` / `spt suspend` / boundary) and their ordering
  invariants. Detecting an orphaned Self after an unexpected harness death needs
  the always-on supervisor — that orphan-watch lands with the daemon (M3),
  carrying the same grace/echo ordering (1.1/3.3) into the supervised path.
- **spt-hosted topology + PTY inject (M3):** the Psyche/echo templates here are
  spawned as detached subprocesses; PTY-hosted injection is `spt-term`/M3.
- **Cross-node Psyche sync + the distributed precedence/freshness rule (M4):**
  M2b lands the **local** direct-write precedence guard (6.5); the node-identity
  + newest-and-newer-than-mine generalization is ADR-0003 / M4.
- **The memformat / Memory-Health / block-OCC merge (deferred):** `docs/DEFERRED.md`
  + `docs/CONTEXT-MEMORY.md` §Deferred. M2b ingests communes as opaque context
  blobs; the structured-block schema + Retrieval Map are a later pass.

**Interim-model note (no daemon — decision 2026-06-01).** CONTEXT describes the
daemon owning all Psyche/pulse loops (`R-DAEMON-1`). The consolidated
`spt-daemon` is **M3**. So M2b mirrors M1/M2a: the long-running **`api listen`**
process (heir to `$LIVE start`) hosts the pulse loop + drop-file watch +
orphan/signoff logic, and the **Psyche is a real subprocess** (per-pid liveness
holds *interim* — CONTEXT §Psyche). M3 swaps the daemon in: Psyche-as-loop,
daemon-authoritative liveness, in-memory seed. **Keep `spt-live` behind clean
seams so the M3 move is a re-host, not a rewrite** (same discipline as M1's
listener and M2a's seed-file).

## Clean-room posture (validated against the sister)

The sister's `src/live/wrapper/` is **deeply Claude-Code-coupled** (`claude -p`,
the orphan/grace state machine wired to CC hooks, `.claude/` drop dirs, the
wrapper owning a `claude` subprocess). For a harness-agnostic core, copy-verbatim
only the harness-orthogonal **mechanism + the hard-won ordering invariants**;
clean-room the rest behind the M2a seams:

- **Copy (mechanism + invariants, ADR-0001 + KNOWN-HAZARDS):** the orphan/grace
  **ordering** (`orphan.rs`: grace recheck *before* compose — 1.1; echo-commune
  *before* INIT_SIGNOFF — 3.3), the stale-signoff sweep (3.2), the drop-file
  single-writer ownership (6.4), the generation/`gen_start = now()` rule (2.4),
  the echo-commune-before-signoff sequence (3.3). These are real bugs already
  paid for once — re-expressed against the spt-store/spt-runtime APIs.
- **Clean-room (becomes `spt-live` over the M2a seams):** everything referencing
  `claude -p`, CC hooks, `.claude` paths, or model selection. The Psyche/echo/
  resume **command lines become the mock adapter's opaque templates**, never
  spt-core code (the model lives in the template — SPT never selects one).

## Sequencing rationale

Crate scaffold first; then the **history subsystem** (the echo-commune consumes
it, so it precedes echo); then **spawn-psyche** (the lifecycle's root — nothing
lives without a Psyche); then **echo-commune** (needs history + a spawn seam);
then **drop-file ingest** (the commune/signoff channel the echo writes into +
the agent writes communes into); then **pulse** (the heartbeat that drives
ingest polling + pending-echo fires); then **signoff/boundary** (composes the
ordering invariants over ingest + echo); then **resume** (re-entry, depends on
history + spawn). Then the `api listen` wiring, the mock lifecycle templates +
LiveAgent E2E, then the activation sweep. Each task tags evidence + activates its
reqs in the same commit; `check` green before the next.

## New requirements to register first (TRACEABILITY rule 3)

**Assessment — mostly none.** The M2b deliverables map onto already-registered
requirements:

- Psyche/echo/resume → `REQ-SEAM-PSYCHE`, `REQ-SEAM-RESUME`, `REQ-SEAM-HISTORY`
  (land inactive; activate per task).
- LiveAgent + Psyche endpoint types → `REQ-EP-1` / `REQ-EP-2` (already active,
  M0 — the type model). M2b makes them *live*; no stage change, build-evidenced.
- Commune/signoff ingest execution → extends `REQ-API-3` to `int`, governed by
  `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (6.4) + `REQ-HAZARD-DIRECT-WRITE-
  PRECEDENCE` (6.5).
- Signoff/orphan ordering → `REQ-HAZARD-GRACE-BEFORE-SIGNOFF` (1.1, graceful
  path), `REQ-HAZARD-STALE-SIGNOFF-SENTINEL` (3.2), `REQ-HAZARD-ECHO-BEFORE-
  SIGNOFF` (3.3).

**The one gap to resolve at plan time:** the **pulse heartbeat** has no dedicated
`REQ-*`. CONTEXT attributes pulse loops to the daemon (`R-DAEMON-1`, M3), but M2b
runs an interim pulse in the `api listen` runner. Two options, decided before T5:
**(a)** treat the interim pulse as mechanism under `REQ-SEAM-PSYCHE` (no new req;
M3's `REQ-DAEMON-*` owns the durable loop), or **(b)** register a thin
`REQ-LIVE-PULSE` if the plan-checker wants the heartbeat independently traced.
**Default: (a)** — the pulse is interim scaffolding M3 subsumes; activating a req
for throwaway glue violates "activate, don't pre-fail." Revisit if T5's pulse
grows load-bearing behavior beyond ingest-polling + pending-echo fires.

## Tasks — `spt-live` (the lifecycle executor)

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| T0 | Scaffold `spt-live` crate (members + deps: spt-proto/store/msg/runtime; serde); activation-prep | new | — | crate compiles; `check` green; layering `…→spt-runtime→spt-live` acyclic |
| T1 | **History subsystem**: `fetch_history(manifest, session)` over the three `[history]` strategies — `fetcher` (bounded `run_bounded`), `locate_normalize` (read raw transcript + bounded normalize), `native` (Path-B store, reuse T7 `history-log`) → normalized records | clean-room; `docs/STORAGE.md` + MANIFEST `[history]` | REQ-SEAM-HISTORY | each strategy yields normalized records from a fixture; a missing/over-time fetcher errors (timeout 5.3), never hangs |
| T2 | **spawn-psyche seam**: spawn the Psyche from `[session.psyche_init]` as a nested `<id>-psyche` perch via `ManifestRuntime`; compose the spt-core-owned `$psyche_prompt` (timestamp + incoming event envelope); set `recursion_guard_env`; bind the Psyche perch (nested, M0 layout) | clean-room (sister `live/wrapper/psyche`) | REQ-SEAM-PSYCHE; REQ-HAZARD-SUBPROCESS-TIMEOUT | a manifest + event spawns a Psyche; `<id>-psyche` perch exists nested; the guard env is set on the child |
| T3 | **echo-commune seam**: spawn `[session.echo_commune]` (cheap-model template) consuming T1 history; recursion-guard so its hooks bail; write the result as a **commune drop-file**, tagged project-primary / live-conservative (provenance `Source:`) | clean-room; `docs/CONTEXT-MEMORY.md` | REQ-SEAM-HISTORY (echo source); REQ-HAZARD-DROP-FILE-SINGLE-WRITER | an echo run consumes history + emits a single-writer commune drop; a recursion-guarded child does not re-echo |
| T4 | **drop-file ingest**: watch the manifest commune/signoff dirs; ingest `<id>-commune.md` / `<id>-signoff.md` into the perch's context store; **spt-core single-writer** (ingest→delete; the mind never deletes); **direct-write precedence** marker (source+timestamp; suppress stale LLM overwrites within the protection window) | clean-room | REQ-API-3 (`int`); REQ-HAZARD-DROP-FILE-SINGLE-WRITER (6.4); REQ-HAZARD-DIRECT-WRITE-PRECEDENCE (6.5) | a dropped commune is ingested + deleted by spt-core; a stale LLM overwrite within the window is suppressed + logged; a direct write always proceeds |
| T5 | **pulse heartbeat**: an interim loop in the `api listen` runner (period = spt-core global config knob, per-endpoint override) driving drop-file polling + pending-echo fires (the M2a `.more-done` gate) + capsule/idle checks | clean-room (sister pulse) | (see "New requirements" — default no new req) | the pulse polls the drop dirs at its period; an armed echo-gate triggers exactly one echo, then clears |
| T6 | **signoff + boundary lifecycle**: graceful signoff (`spt shutdown` / `spt suspend`) runs the echo-commune **before** composing INIT_SIGNOFF (3.3); grace recheck binds `still_gone` **before** any signoff write (1.1); **stale-signoff sweep** on listener start (3.2); boundary authors the **Self-resume commune** (M2a `boundary` rebinds; M2b writes the resume commune) | copy invariants (sister `orphan.rs`) | REQ-HAZARD-ECHO-BEFORE-SIGNOFF (3.3); REQ-HAZARD-GRACE-BEFORE-SIGNOFF (1.1); REQ-HAZARD-STALE-SIGNOFF-SENTINEL (3.2) | echo precedes signoff (asserted by order); a stale signoff sentinel is swept and never tears down a fresh start; a recovered Self after grace is **not** signed off |
| T7 | **resume-session seam**: `fresh-with-preload` (spawn a cleared session + `$psyche-context` download) and `continue-existing` (resume `$session_id`); wire into spawn + boundary re-entry | clean-room; CONTEXT §resume seam | REQ-SEAM-RESUME | a resume template re-enters an existing session; a fresh-with-preload launches cleared + preloaded; the wrong/missing key errors before spawn |

## Tasks — `spt` binary + integration + infra

| # | Task | Reqs | Acceptance |
|---|------|------|------------|
| T8 | **`api listen` lifecycle wiring**: extend the M2a relay runner to host the LiveAgent lifecycle — spawn the Psyche (T2), run the pulse (T5), ingest drops (T4), fire echoes (T3), and execute graceful signoff (T6). Keep the surface thin (M3 re-hosts into the daemon) | REQ-SEAM-PSYCHE, REQ-API-2 | `api listen` on a live-capable manifest spawns + hosts a Psyche; teardown is graceful |
| T9 | **LiveAgent E2E**: extend `adapters/mock/manifest.toml` with `[session.psyche_init/psyche_resume/echo_commune/signoff]` + `[history]` (native) + commune/signoff dirs; prove end-to-end: spawn LiveAgent → Psyche nested perch appears → drop a commune → ingested+deleted → graceful signoff fires echo **before** teardown | REQ-SEAM-PSYCHE (`int`), REQ-SEAM-HISTORY (`int`), REQ-API-3 (`int`) | a `tests/` integration test proves the full lifecycle via the mock adapter, harness-agnostic |
| T10 | **Activation sweep**: activate M2b reqs + hazards; activate `REQ-EP-1/2` live-evidence (no stage change); green `check`, CI green; amend ROADMAP (M2b delivered) + CONTEXT/M2b notes; author the **M3 plan** stub | — | `check` green with all M2b reqs activated; CI matrix + gate pass |

## M2b requirement-activation map

Activate as each task lands (default `["impl","unit"]`; the lifecycle seams add
`int` at T9):

- **Seams:** REQ-SEAM-HISTORY (`impl`+`unit`, T1; +`int` T9), REQ-SEAM-PSYCHE
  (`impl`+`unit`, T2; +`int` T9), REQ-SEAM-RESUME (`impl`+`unit`, T7).
- **api surface:** REQ-API-3 gains `int` (T4 ingest / T9), REQ-API-2 reused (no
  change — `api listen` already covered M2a).
- **Hazards:** REQ-HAZARD-DROP-FILE-SINGLE-WRITER (T4), REQ-HAZARD-DIRECT-WRITE-
  PRECEDENCE (T4), REQ-HAZARD-ECHO-BEFORE-SIGNOFF (T6), REQ-HAZARD-GRACE-BEFORE-
  SIGNOFF (T6, graceful path), REQ-HAZARD-STALE-SIGNOFF-SENTINEL (T6). REQ-HAZARD-
  SUBPROCESS-TIMEOUT reused (M2a) for T1/T2 spawns — no change.
- **Endpoint types:** REQ-EP-1 / REQ-EP-2 already active (M0) — M2b makes
  LiveAgent + Psyche *live*; build-evidenced, no stage change.

**Stay `[]` (M3):** REQ-START-3 (spt-hosted), REQ-SEAM-UPDATE, REQ-DAEMON-*,
REQ-HAZARD-DAEMON-HOSTED-LIVENESS, REQ-HAZARD-CONPTY-DSR, REQ-HAZARD-RESTART-
IDEMPOTENT, REQ-TERM-*. **Stay `[]` (M4/M5):** all R-NET/R-PAIR, REQ-EP-4
(PresenceChannel impl), the distributed precedence/freshness generalization.

## Workspace change

Add `crates/spt-live` to `members`. Layering stays acyclic (R-ARCH-1):
`spt-proto → spt-store → spt-msg → spt-runtime → spt-live`; the `spt` binary
(top consumer) gains the `api listen` lifecycle wiring dispatching into
`spt-live`. `spt-live` is **not** part of the public SDK surface (R-ARCH-2 is
proto/runtime/msg) — it is the daemon-bound executor M3 consolidates.

## Risks carried into M2b

- **Interim pulse vs daemon loop.** The T5 pulse is throwaway glue M3's daemon
  subsumes — keep it behind one small module so the M3 re-host is localized
  (mirror M2a's seed-file discipline). Don't over-build it (the "New
  requirements" gap above).
- **Ordering invariants are the load-bearing carryover.** 1.1 (grace before
  signoff) and 3.3 (echo before signoff) are the sister's most expensive bugs.
  Copy the *ordering*, clean-room the wiring; each gets an explicit ordering
  assertion test, not just a happy-path test.
- **Drop-file single-writer across the harness boundary (6.4).** The mind (an
  LLM session) must be read-only on drop files; only spt-core deletes. Bake the
  ownership into the runtime contract + assert the mind-never-deletes path.
- **Real-process Psyche is interim (CONTEXT §Psyche / 2.5).** Per-pid liveness
  holds only until M3. Don't let liveness checks leak `is_process_alive(info.pid)`
  assumptions into durable state — gate them behind the interim seam so M3's
  daemon-authoritative `status` field is a swap.
- **Echo-commune leak (CONTEXT-MEMORY anti-leak).** Echo communes are
  project-primary / live-conservative; tag provenance (`Source:`) at ingest so
  the later memformat pass (deferred) inherits correct tiering. Getting
  provenance right now is cheap; retrofitting it is not.
- **No architecture risk.** M2b is the lifecycle executor on the proven
  M0/M1/M2a substrate, in the same no-daemon interim model. The FATALs were the
  daemon (ADR-0004, M3) and pairing (ADR-0005, M4).
