Edit

//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.

pub mod manifest;

//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.
//
// spt-runtime joins spt-proto + spt-msg as the third member of the public,
// semver-committed SDK surface (R-ARCH-2): a harness author programs against
// this crate's Manifest/AgentRuntime types.
// [impl->REQ-ARCH-2]

pub mod manifest;


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

//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.
//
// spt-runtime joins spt-proto + spt-msg as the third member of the public,
// semver-committed SDK surface (R-ARCH-2): a harness author programs against
// this crate's Manifest/AgentRuntime types.
// [impl->REQ-ARCH-2]

pub mod manifest;

//! spt-core harness contract — outbound half (M2a).
//!
//! `spt-runtime` makes spt-core **drivable by a manifest-described harness** with
//! zero Claude Code (or any harness) conventions in-repo. It is the fourth layer
//! in the acyclic stack (`spt-proto -> spt-store -> spt-msg -> spt-runtime`).
//!
//! Two surfaces land here across M2a:
//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and
//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template
//!   substitution + spawn-session seam, T2).
//!
//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.

pub mod manifest;
pub mod runtime;

pub use manifest::{Manifest, ManifestError};
pub use runtime::{AgentRuntime, ManifestRuntime, RuntimeError, Spawned};

+// spt-runtime joins spt-proto + spt-msg as the third member of the public,

+// semver-committed SDK surface (R-ARCH-2): a harness author programs against

+// this crate's Manifest/AgentRuntime types.


id = "REQ-ARCH-2"
title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
required_stages = []

id = "REQ-ARCH-2"
title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
required_stages = ["impl"]


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

id = "REQ-ARCH-2"
title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
required_stages = []

id = "REQ-ARCH-2"
title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
required_stages = ["impl"]

title = "Wire-protocol version independent of crate semver, N-1 compat window"


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && traceable-reqs check 2>&1 | grep -E "ARCH-2"; echo "full check exit:"; traceable-reqs check >/dev/null 2>&1; echo $?; echo "=== M2a activated reqs summary ==="; traceable-reqs check 2>&1 | grep -E "MANIFEST-1|SEAM-SPAWN|SEAM-POSTSPAWN|SEAM-ACTIVITY|SEAM-INJECT|SEAM-CAPABILITY|API-1 |API-2 |API-3 |START-1|START-2|START-4|LOCAL-API-AUTH|CASCADE|SUBPROCESS|ARCH-2" | grep "+impl"

Verify ARCH-2 + M2a req summary


  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
full check exit:
0
=== M2a activated reqs summary ===
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-API-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
  [OK] REQ-HAZARD-CASCADE-WIPE-GUARD  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-LOCAL-API-AUTH  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-SUBPROCESS-TIMEOUT  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-MANIFEST-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-SEAM-ACTIVITY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-CAPABILITY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-INJECT  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-POSTSPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-SPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-START-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-START-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-START-4  required: [impl, unit]  stages: -doc +impl +unit -int


T0: Scaffold spt-runtime crate

New crate (members + deps spt-proto/store/msg, serde, toml); activation-prep. Compiles; traceable-reqs check green.

T1: Manifest schema + parse + validate

Typed structs for every docs/MANIFEST.md section + parse + validate (helpful errors). REQ-MANIFEST-1.

T2: AgentRuntime trait + ManifestRuntime

Substitution-key catalog + opaque-template fill + spawn-session seam (timeout). REQ-SEAM-SPAWN, REQ-HAZARD-SUBPROCESS-TIMEOUT.

T3: spt api dispatch group + local-api-auth

--manifest/--adapter resolution, adapter_name on every call, exit codes, local-api-auth guard. REQ-API-1, REQ-HAZARD-LOCAL-API-AUTH.

T4: Harness-hosted startup (seed/listen/bind)

api seed (interim seed-file by parent_pid) + api listen (consume seed, validate session_id, bind skeleton->live, relay loop) + api bind. REQ-START-1/2, REQ-SEAM-POSTSPAWN, REQ-API-2, REQ-HAZARD-WINDOWS-PID-RECYCLE.

T5: Delivery + activity (state/poll/inject)

api state busy|idle (+echo-gate sentinel, --no-gate, echo-gate set/clear) + api poll [--include-deferred] + inject-method selection. REQ-SEAM-ACTIVITY, REQ-SEAM-INJECT, REQ-API-2.

T6: Worker perches + cascade-wipe guard

api worker-start/worker-stop/worker-poll + cascade-wipe guard. REQ-API-2, REQ-HAZARD-CASCADE-WIPE-GUARD.

T7: Reporting + boundary surface

api boundary clear|compact + session-end (--erase) + presence + history-log + emit stub + capability hostable_types + env-alias supply. REQ-API-2/3, REQ-START-4, REQ-SEAM-CAPABILITY, REQ-HAZARD-SOFT-CLEANUP.

T8: Generic mock adapter

adapters/mock/manifest.toml + cross-platform session scripts/helper bin; zero CC conventions. REQ-MANIFEST-1, REQ-API-3.

T9: Contract E2E test

load mock manifest -> ManifestRuntime spawns session -> api bind -> api state idle -> api poll delivers spooled message. REQ-API-1/2 int, REQ-START-2 int.

T10: Activation sweep

Activate M2a reqs + hazards + REQ-ARCH-2, green check, CI green, amend ROADMAP/CONTEXT for split.


All M2a reqs green. Amend ROADMAP to mark M2a delivered.


28	- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
29	- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
30	  - **M2a — Harness contract.** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface + the **harness-hosted** startup topology (seed→listen→bind) + mock adapter + contract E2E. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
31	  - **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). JIT plan authored when M2a lands.
32	  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
33	- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update.
34	- **M4 — Networking + instances.** `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
35	- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.

- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
  - **M2a — Harness contract.** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface + the **harness-hosted** startup topology (seed→listen→bind) + mock adapter + contract E2E. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
  - **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). JIT plan authored when M2a lands.
  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update.
- **M4 — Networking + instances.** `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.


Edit

- **M2a — Harness contract.** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface + the **harness-hosted** startup topology (seed→listen→bind) + mock adapter + contract E2E. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).

- **M2a — Harness contract. ✅ delivered (2026-06-01).** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface (seed/listen/bind/state/echo-gate/poll/worker-*/boundary/session-end/presence/history-log/emit/capability) + the **harness-hosted** startup topology (seed→listen→bind, interim seed-file) + local-api-auth + the generic mock adapter + contract E2E. All M2a reqs activated + green. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).


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

# spt-core Roadmap

> Lightweight-but-structured path from design → fruition. Not a GSD/phase-gated process — a clear sequence with de-risk gates. Planning corpus is complete (PRD + CONTEXT + 5 ADRs + 6 design docs); this is how we build it.

## End goal

A harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, **first-class P2P networking**, and a runtime-manifest harness contract — shipped as Rust crates + a single `spt` binary. The proof: the same agent runs natively across all of a user's machines with a synced mind, reachable and operable from anywhere, with seamless self-update and zero-config cross-machine messaging. First thing built atop it: a rebuilt `spt` plugin at parity with today's modern SPT.

## Scope decisions (locked)

- **v1 = the full PRD, networking included.** The networked element is the *reason* spt-core exists; it is not deferrable to v1.1.
- **The hard freeze on `claude_skill_owl` is not a clock.** It works today and delivers value as-is while spt-core is built — no schedule pressure from it.
- **Process: lightweight but structured.** GSD is too heavy. This roadmap + the design docs are the structure.

## Path to fruition (sequence)

**Stage A — Red-team review (before building).** Pressure-test the design adversarially. Focus on the riskiest architectural decisions, not bikeshedding. Tools: `plan-eng-review` (architecture) + `codex` (adversarial second opinion). Scope-cutting is *not* a goal (v1 is locked as full PRD), but surfacing contradictions / over-engineering / unproven assumptions is.

**Stage B — De-risk spikes.** Throwaway proofs of the load-bearing unknowns — anything that, if it fails, forces a redesign. Do these before committing to the full build. See *De-risk spikes* below.

**Stage C — Walking skeleton.** Build the killer-quickstart slice end-to-end through the real crate boundaries: **two agents exchange a message** (first local; ultimately cross-node — the real proof). Validates the whole stack early; then thicken.

**Stage D — Phased build (M0–M5).** Dependency-ordered, each milestone ships something usable.

## Milestones (all v1)

- **M0 — Workspace + commodity layer.** Cargo workspace; `spt-proto` (wire envelope grammar, endpoint types, framing, Ed25519 identity, typed+binary payloads) + `spt-store` (spool, perch layout, trust store, registry persistence). Copy-verbatim the stable formats from the sister project (ADR-0001).
- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
  - **M2a — Harness contract.** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface + the **harness-hosted** startup topology (seed→listen→bind) + mock adapter + contract E2E. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
  - **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). JIT plan authored when M2a lands.
  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update.
- **M4 — Networking + instances.** `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.

## De-risk spikes (do early — these can invalidate the architecture)

1. **Broker/brain no-terminate handoff** — prove a PTY child + open socket survive a daemon-*logic* restart (FD-passing or stable-broker shim). The hardest invariant in the design (ADR-0004).
2. **Iroh + mDNS smoke test** — two nodes, separate NATs, one message end-to-end; measure direct-vs-relay + binary size (research brief §5). Validates the whole networking premise.
3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.

## Immediate next steps

Progress (2026-05-31):
- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
- ✅ **Spike #1 (broker/brain handoff)** — PASS on Windows ConPTY. `docs/spikes/SPIKE-01-broker-handoff.md`. Found+fixed the ConPTY DSR-stall hazard. 4 open gaps carried to M3.
- ✅ **Spike #2 (Iroh smoke test)** — PASS (single-host). `docs/spikes/SPIKE-02-iroh-smoke.md`. iroh 0.98 API workable; 13.57 MB release. Cross-NAT/relay proof deferred to M4.
- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).

Remaining before M0:
1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).
2. **Spike #4 (manifest-driven lifecycle)** — a manifest drives one real session spawn→bind→commune→signoff. (Spike #3 ConPTY hosting is largely covered by Spike #1.)
3. Resolve the open user decisions logged in the ADR amendments (context-merge model, seed-rotation, R-UPD-3 rewording).
4. **M0** (workspace + `spt-proto` + `spt-store`) — **started**; task breakdown in [`M0-PLAN.md`](./M0-PLAN.md). Each milestone gets its own just-in-time plan (this one is M0); the roadmap stays the milestone sequence, the JIT plan is the task layer.

## Tooling: requirement traceability (adopted)

`traceable-reqs` is adopted as the requirement-tracing gate. `traceable-reqs.toml` seeds the `REQ-*` registry from PRD `R-*` + KNOWN-HAZARDS invariants (all inactive until milestone activation). The development contract + enforcement (CI gate, milestone activation, agent rule, quality audit) is `docs/TRACEABILITY.md`; the agent rule lives in `CLAUDE.md`. **M0 task:** install the CLI, validate the seed, wire the CI workflow, activate M0 reqs.

## Open threads carried forward

- New deferred items logged this session (`docs/DEFERRED.md`): block-level OCC context merge; the rebalance / Memory-Health pass; the consent framework's gated capabilities.
- The dual-audience docs strategy (`docs/DOCS-STRATEGY.md`) governs the *shipped* docs, authored as code lands.

-  - **M2a — Harness contract.** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface + the **harness-hosted** startup topology (seed→listen→bind) + mock adapter + contract E2E. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).

+  - **M2a — Harness contract. ✅ delivered (2026-06-01).** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface (seed/listen/bind/state/echo-gate/poll/worker-*/boundary/session-end/presence/history-log/emit/capability) + the **harness-hosted** startup topology (seed→listen→bind, interim seed-file) + local-api-auth + the generic mock adapter + contract E2E. All M2a reqs activated + green. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).


Run the full CI gate locally (build + test + clippy + traceability), then commit T10.


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && echo "=== build ===" && cargo build --workspace --all-targets 2>&1 | tail -2 && echo "=== clippy ===" && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -2 && echo "=== test ===" && cargo test --workspace 2>&1 | grep -E "test result: FAILED|error\[" | head; echo "(no failures above = pass)" && echo "=== traceability ===" && traceable-reqs check >/dev/null 2>&1 && echo "check: PASS"

Full CI gate locally


=== build ===
   Compiling mock-adapter v0.0.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.00s
=== clippy ===
    Checking mock-adapter v0.0.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s
=== test ===
(no failures above = pass)
=== traceability ===
check: PASS


cd "C:\Users\decid\Documents\projects\spt-core" && cargo test --workspace 2>&1 | grep -c "test result: ok" && echo "--- total tests ---" && cargo test --workspace 2>&1 | grep -oE "[0-9]+ passed" | awk '{s+=$1} END {print s" passed"}'

Count passing tests


13
--- total tests ---
162 passed


162 tests green. Run advisory lint, then commit T10.


cd "C:\Users\decid\Documents\projects\spt-core" && traceable-reqs lint 2>&1 | tail -20

Advisory traceability lint


- Required stages:

### REQ-HAZARD-DAEMON-HOSTED-LIVENESS
- Title: Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)
- Required stages:

## How to report back

For every (requirement, failing criterion) pair, emit one finding:

    {
      "code": "requirement_quality",
      "requirementId": "REQ-...",
      "criterion": "singular" | "verifiable" | "atomic" | "active-voice",
      "message": "<short reason>",
      "suggestedRevision": "<optional rewrite>"
    }

Wrap your response as { "findings": [ ... ] } listing only your concerns; the
deterministic findings above don't need to be repeated.


Advisory lint is the LLM-assisted layer (CI `|| true`); the deterministic gate passed. Commit T10.


cd "C:\Users\decid\Documents\projects\spt-core" && git add -A && git commit -q -F - <<'EOF'
chore(M2a): T10 — activation sweep + ROADMAP (M2a delivered)

- REQ-ARCH-2 activated [impl]: spt-runtime joins spt-proto + spt-msg as the
  public SDK surface (tagged in spt-runtime/lib.rs). REQ-ARCH-1 gains a fourth
  crate (build-evidenced, no stage change).
- ROADMAP: M2a marked ✅ delivered (2026-06-01).

M2a complete. All activated reqs green across the full gate:
- cargo build --workspace --all-targets
- cargo test --workspace (162 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- traceable-reqs check

Activated this milestone: REQ-MANIFEST-1 (doc+impl+unit), REQ-SEAM-SPAWN,
REQ-API-1/2/3, REQ-START-1/2/4, REQ-SEAM-POSTSPAWN/ACTIVITY/INJECT/CAPABILITY,
REQ-HAZARD-LOCAL-API-AUTH/CASCADE-WIPE-GUARD/SUBPROCESS-TIMEOUT, REQ-ARCH-2
(+ int on API-1/2 + START-2). Next: M2b (spt-live lifecycle) JIT plan.

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

Commit T10 + show M2a history


warning: in the working copy of 'ROADMAP.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
dbdbabe chore(M2a): T10 — activation sweep + ROADMAP (M2a delivered)
504b346 test(spt): M2a T9 — contract E2E through the mock adapter
146a7b8 feat(mock-adapter): M2a T8 — generic mock adapter (manifest + helper)
d555c93 feat(spt): M2a T7 — reporting + boundary surface
42af360 feat(spt): M2a T6 — worker perches + cascade-wipe guard
bae7124 feat(spt): M2a T5 — delivery + activity (state / echo-gate / poll / inject)
d6610b6 feat(spt): M2a T4 — harness-hosted startup (seed → listen → bind)
281eb8d feat(spt): M2a T3 — `spt api` dispatch group + local-api-auth
fecd6d2 feat(spt-runtime): M2a T2 — AgentRuntime seam + ManifestRuntime
ae23376 feat(spt-runtime): M2a T1 — runtime-manifest schema + parse + validate
6741453 feat(spt-runtime): M2a T0 — scaffold harness-contract crate


T0: Scaffold spt-runtime crate

T1: Manifest schema + parse + validate

T6: Worker perches + cascade-wipe guard

T7: Reporting + boundary surface

T8: Generic mock adapter

T9: Contract E2E test

T10: Activation sweep


Updated task #11 status


M2a complete. All 11 tasks (T0–T10) landed, each as an atomic commit with evidence tagged in-change.

## What shipped

**`spt-runtime` crate (outbound contract)**
- Manifest schema — typed structs for every `MANIFEST.md` section, parse+validate with helpful errors (unknown enum variant names the bad value); harness/shell kinds, all closed vocabularies as enums.
- `AgentRuntime` trait + `ManifestRuntime` — opaque-template `{key}` fill (missing key errors *before* spawn), quote-aware tokenizer, detached spawn, and a bounded runner with a **mandatory timeout** (hung child killed+reaped, never hangs — hazard 5.3).

**`spt` binary — `api` inbound surface**
- Dispatch group with mandatory `--adapter`, manifest load+validate, **local-api-auth** (capability token or session_id, constant-time compare — codex #13).
- Startup: `seed → listen → bind` (interim seed-file in `spt-store::seed`, the localized M3 swap point), `proc::parent_pid()` anchor, pid-recycle guard (5.1).
- Delivery: `state`/`echo-gate` sentinels, `poll`, inject-method resolution (drops PTY→M3).
- Workers: `worker-start/stop/poll` + cascade-wipe guard (6.3).
- Reporting: `boundary`, `session-end` (soft default, `--erase` guarded), `presence`, `history-log`, `emit` stub, `capability`, file-drop + env-alias resolution.

**`adapters/mock`** — the only adapter-shaped artifact in-tree, zero vendor conventions (grep-enforced); manifest + `mock-session` helper.

**Contract E2E** — two tests prove both topologies end-to-end through the mock adapter (spawn→bind→state→poll, and seed→listen→relay).

## Gate (all green)
- `cargo build/test --workspace` — **162 tests passing**
- `cargo clippy --workspace --all-targets -- -D warnings`
- `traceable-reqs check` — 17 M2a requirements activated and covered

**Next:** M2b (`spt-live` lifecycle) — its JIT plan is authored when M2a lands, per the roadmap.


write the JIT plan for M2b now


<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>


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


Need to ground M2b plan in the lifecycle model. Read the source docs.


cd "C:\Users\decid\Documents\projects\spt-core" && grep -n "REQ-SEAM-PSYCHE\|REQ-SEAM-RESUME\|REQ-SEAM-HISTORY\|REQ-HAZARD-DROP-FILE-SINGLE-WRITER\|REQ-HAZARD-STALE-SIGNOFF\|REQ-EP-\|REQ-LIVE\|REQ-PSYCHE\|REQ-MEM\|REQ-PULSE" -A2 traceable-reqs.toml | head -80

Find M2b-relevant requirements


71:id = "REQ-SEAM-PSYCHE"
72-title = "spawn-psyche seam (fresh + resume templates)"
73-required_stages = []
--
75:id = "REQ-SEAM-HISTORY"
76-title = "History subsystem (fetcher / locate-normalize / native store)"
77-required_stages = []
--
87:id = "REQ-SEAM-RESUME"
88-title = "resume-session seam (fresh-with-preload / continue-existing)"
89-required_stages = []
--
133:id = "REQ-EP-1"
134-title = "Day-one endpoint types; open type system"
135-required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
--
137:id = "REQ-EP-2"
138-title = "Agent endpoints vs Shells distinction in the type model"
139-required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
--
141:id = "REQ-EP-3"
142-title = "Messaging payloads carry typed operation commands + file blobs"
143-required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
--
145:id = "REQ-EP-4"
146-title = "PresenceChannel broker endpoint (seam day-one)"
147-required_stages = []
--
372:id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
373-title = "Stale signoff sentinel does not kill a fresh start (3.2)"
374-required_stages = []
--
432:id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
433-title = "Drop files are daemon-owned single-writer (6.4)"
434-required_stages = []


cd "C:\Users\decid\Documents\projects\spt-core" && grep -niE "psyche|pulse|commune|signoff|echo-commune|echo commune|live agent|live_agent|companion" CONTEXT.md | head -50

Locate lifecycle model in CONTEXT


5:**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.
26:The single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.
30:- **daemon brain** ("userspace") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.
48:spt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.
57:A declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.
75:How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.
81:**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first commune populates it).
82:- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.
88:**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.
90:**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:
93:- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.
106:- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook.
113:There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:
118:- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.
119:- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.
129:- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.
132:- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`).
138:**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.
144:**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:
147:3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.
156:4. Psyche is daemon-spawned, same as above.
167:Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's "ready agent".
170:A Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's "live agent".
172:**Psyche**:
173:The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **Liveness is daemon-held, not PID-based.** Unlike the sister's separate wrapper *process*, the Psyche (under ADR-0004) is a **loop inside the daemon** — it has no dedicated liveness pid, and its summarizer subprocess is ephemeral. Its perch is alive iff the daemon's endpoint table says so, surfaced via a daemon-managed `status` field on `info.json` (the same pattern Shells use), **never** an `is_process_alive(info.pid)` check. The M1/M2a interim (no daemon) keeps it a real process, so per-pid liveness holds *interim*; M3 replaces it with daemon-authoritative liveness (KNOWN-HAZARDS 2.5).
197:**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.
233:**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content channels).
285:### Cross-node Psyche sync
315:- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.
334:**Commands** (user/agent-initiated lifecycle, unprefixed): `spt suspend <id[@node]>` (suspend any of your instances from any node), `spt wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).
336:**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).
343:- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.
357:When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
365:**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.
376:- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
384:Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).
430:- **Ungated:** remote-drive of your *own running* instance (low risk — your own instance; a light "node X is driving `ling@desktop`" notification suffices), cross-node context/Psyche sync (your own data).
453:**Legacy migration at install:** standalone install **auto-detects an existing `claude_skill_owl` (modern SPT) install and offers migration** (the migration commitment above) — identity, agents, tracked Psyche context.
469:This is the realization of the "guided resume" (resume by project or agent, XMB-style filtered selection) and "management GUI" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.


cd "C:\Users\decid\Documents\projects\spt-core" && echo "=== all req ids ===" && grep -E '^id = ' traceable-reqs.toml | sed 's/id = //' | tr -d '"' | tr '\n' ' '; echo; echo "=== hazards 3.x + 6.4 ==="; sed -n '/### 3.1/,/### 4.1/p' docs/KNOWN-HAZARDS.md | grep -E "###|Failure:|Invariant:|spt-core mapping:" | head -40

Enumerate reqs + lifecycle hazards


=== all req ids ===
REQ-ARCH-1 REQ-ARCH-2 REQ-ARCH-3 REQ-ARCH-4 REQ-DAEMON-1 REQ-DAEMON-2 REQ-DAEMON-3 REQ-DAEMON-4 REQ-MANIFEST-1 REQ-SEAM-SPAWN REQ-SEAM-POSTSPAWN REQ-SEAM-PSYCHE REQ-SEAM-HISTORY REQ-SEAM-ACTIVITY REQ-SEAM-INJECT REQ-SEAM-RESUME REQ-SEAM-CAPABILITY REQ-SEAM-UPDATE REQ-API-1 REQ-API-2 REQ-API-3 REQ-START-1 REQ-START-2 REQ-START-3 REQ-START-4 REQ-EP-1 REQ-EP-2 REQ-EP-3 REQ-EP-4 REQ-INST-1 REQ-INST-2 REQ-INST-3 REQ-INST-4 REQ-INST-5 REQ-INST-6 REQ-INST-7 REQ-INST-8 REQ-REACH-1 REQ-REACH-2 REQ-MSG-1 REQ-MSG-2 REQ-MSG-3 REQ-NODE-IDENTITY REQ-NET-1 REQ-NET-2 REQ-NET-3 REQ-PAIR-1 REQ-PAIR-2 REQ-PAIR-3 REQ-PAIR-4 REQ-UPD-1 REQ-UPD-2 REQ-UPD-3 REQ-UPD-4 REQ-UPD-5 REQ-TERM-1 REQ-TERM-2 REQ-TERM-3 REQ-FRONT-1 REQ-INSTALL-1 REQ-INSTALL-2 REQ-INSTALL-3 REQ-MIGRATE-1 REQ-INFRA-1 REQ-DOCS-1 REQ-DOCS-2 REQ-DOCS-3 REQ-DOCS-4 REQ-DOCS-5 REQ-HAZARD-GRACE-BEFORE-SIGNOFF REQ-HAZARD-INFO-JSON-TORN-READ REQ-HAZARD-STALE-INDEX-LOCK REQ-HAZARD-DEFERRED-DRAIN REQ-HAZARD-WORKER-PATH REQ-HAZARD-PARENT-PID-PREFER REQ-HAZARD-STDIN-SESSION-ID REQ-HAZARD-HANDOFF-ARGV-COMPAT REQ-HAZARD-GEN-START-NOW REQ-HAZARD-EPHEMERAL-CLEANUP REQ-HAZARD-STALE-SIGNOFF-SENTINEL REQ-HAZARD-ECHO-BEFORE-SIGNOFF REQ-HAZARD-ENVELOPE-DECODE-ORDER REQ-HAZARD-ENVELOPE-PARSER-SAFE REQ-HAZARD-EVENTPART-REASSEMBLY REQ-HAZARD-REGISTRY-STALE-CLEAN REQ-HAZARD-DEFERRED-SURVIVE-DRAIN REQ-HAZARD-INBOX-NO-DOUBLE REQ-HAZARD-WINDOWS-PID-RECYCLE REQ-HAZARD-EBUSY-RENAME REQ-HAZARD-SUBPROCESS-TIMEOUT REQ-HAZARD-UNC-PATH-STRIP REQ-HAZARD-SINGLE-PATH-SOURCE REQ-HAZARD-SOFT-CLEANUP REQ-HAZARD-CASCADE-WIPE-GUARD REQ-HAZARD-DROP-FILE-SINGLE-WRITER REQ-HAZARD-DIRECT-WRITE-PRECEDENCE REQ-HAZARD-CONPTY-DSR REQ-HAZARD-LOCAL-API-AUTH REQ-HAZARD-RESTART-IDEMPOTENT REQ-HAZARD-UPDATE-ROLLBACK REQ-HAZARD-DAEMON-HOSTED-LIVENESS
=== hazards 3.x + 6.4 ===
### 3.1 Ephemeral perch cleanup on every `ring` exit path
- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.
- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.
- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.
### 3.2 Stale signoff sentinel must not kill a fresh start
- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.
- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.
- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.
### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.
- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.
- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.
### 4.1 Envelope HTML-entity codec ordering — `&` decoded LAST


cd "C:\Users\decid\Documents\projects\spt-core" && grep -nE "GRACE-BEFORE-SIGNOFF|DIRECT-WRITE-PRECEDENCE|ECHO-BEFORE-SIGNOFF|DROP-FILE-SINGLE-WRITER|STALE-SIGNOFF" traceable-reqs.toml; echo "=== 6.4/6.5 ==="; sed -n '/### 6.4/,/## 7/p' docs/KNOWN-HAZARDS.md | grep -E "###|Failure:|Invariant:|mapping:"; echo "=== pulse/echo CONTEXT-MEMORY ==="; grep -niE "pulse|echo commune|signoff|^##|catch-up|revive" docs/CONTEXT-MEMORY.md | head -30

Read remaining hazards + memory model headers


332:id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
372:id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
376:id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
432:id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
436:id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
=== 6.4/6.5 ===
### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.
- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.
### 6.5 Direct-write precedence guard against stale LLM overwrites
- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.
- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity, and the freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the distributed generalization of this guard. Highest-value carryover for the sync design.
=== pulse/echo CONTEXT-MEMORY ===
5:## Two-tier context
12:### The discriminator (applied per *topic*, not per fragment)
16:### Anti-leak rules
23:4. **Commune-source asymmetry** (the structural fix): the live signal is strong only from **Self-authored communes**; **echo communes are project-centric** (they synthesize project-heavy session logs). So **echo communes are project-context-primary and live-context-conservative** — they make few/no live edits. This kills the leak at its source. Provenance is recorded on each block (`Source:`).
25:### Boundary communes are Self-authored (resume Self, not echo)
31:- **`spt suspend`** → resume Self → **signoff** file-drop (suspend = the endpoint going down, so it's a signoff, not a commune).
35:### `spt refresh` (spt-hosted self-service context refresh)
39:### Ownership
43:## memformat (deeply integrated, self-evolving, the split scaffold)
52:### Structured blocks (adapted from pi-agent-memory)
57:## <stable-block-id>
72:## Retrieval Map (generated INDEX)
76:## Start-injection: dense working set, sparse tail
85:## Deferred
90:## Not adopted


cd "C:\Users\decid\Documents\projects\spt-core" && grep -niE "pulse|R-LIVE|live agent|heartbeat|R-MEM|companion|R-PSY" PRD.md | head -20; echo "=== grace hazard text ==="; sed -n '/id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"/,+2p' traceable-reqs.toml; grep -niE "grace|2.4|GEN-START|generation" docs/KNOWN-HAZARDS.md | head

Check PRD for pulse/live reqs + grace hazard


58:- **R-DAEMON-1** Exactly **one logical `spt-daemon` per machine**, always-on, owning PTYs, network identity + WAN endpoint, subnet registry, all spools, all poll-listener logic, and all Psyche/pulse loops. No separate listener or wrapper processes (ADR-0004).
=== grace hazard text ===
id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
required_stages = []
11:### 1.1 Phantom INIT_SIGNOFF after grace period
12:- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.
13:- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
14:- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.
15:- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.
19:- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).
63:### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
65:- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes "same gen continuing" vs "new gen born".
66:- **spt-core mapping:** carries to the daemon's per-instance generation tracking.
83:- **Sister cite:** `src/owl/ring.rs:58-294`.


Grounded. Writing `M2b-PLAN.md`, mirroring the M2a-PLAN structure.


Write

# 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).


File created successfully at: C:\Users\decid\Documents\projects\spt-core\M2b-PLAN.md (file state is current in your context — no need to Read it back)

# 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).


Link the plan from ROADMAP's M2b bullet, then commit.


Edit

- **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). JIT plan authored when M2a lands.

- **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).


# spt-core Roadmap

> Lightweight-but-structured path from design → fruition. Not a GSD/phase-gated process — a clear sequence with de-risk gates. Planning corpus is complete (PRD + CONTEXT + 5 ADRs + 6 design docs); this is how we build it.

## End goal

A harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, **first-class P2P networking**, and a runtime-manifest harness contract — shipped as Rust crates + a single `spt` binary. The proof: the same agent runs natively across all of a user's machines with a synced mind, reachable and operable from anywhere, with seamless self-update and zero-config cross-machine messaging. First thing built atop it: a rebuilt `spt` plugin at parity with today's modern SPT.

## Scope decisions (locked)

- **v1 = the full PRD, networking included.** The networked element is the *reason* spt-core exists; it is not deferrable to v1.1.
- **The hard freeze on `claude_skill_owl` is not a clock.** It works today and delivers value as-is while spt-core is built — no schedule pressure from it.
- **Process: lightweight but structured.** GSD is too heavy. This roadmap + the design docs are the structure.

## Path to fruition (sequence)

**Stage A — Red-team review (before building).** Pressure-test the design adversarially. Focus on the riskiest architectural decisions, not bikeshedding. Tools: `plan-eng-review` (architecture) + `codex` (adversarial second opinion). Scope-cutting is *not* a goal (v1 is locked as full PRD), but surfacing contradictions / over-engineering / unproven assumptions is.

**Stage B — De-risk spikes.** Throwaway proofs of the load-bearing unknowns — anything that, if it fails, forces a redesign. Do these before committing to the full build. See *De-risk spikes* below.

**Stage C — Walking skeleton.** Build the killer-quickstart slice end-to-end through the real crate boundaries: **two agents exchange a message** (first local; ultimately cross-node — the real proof). Validates the whole stack early; then thicken.

**Stage D — Phased build (M0–M5).** Dependency-ordered, each milestone ships something usable.

## Milestones (all v1)

- **M0 — Workspace + commodity layer.** Cargo workspace; `spt-proto` (wire envelope grammar, endpoint types, framing, Ed25519 identity, typed+binary payloads) + `spt-store` (spool, perch layout, trust store, registry persistence). Copy-verbatim the stable formats from the sister project (ADR-0001).
- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
  - **M2a — Harness contract. ✅ delivered (2026-06-01).** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface (seed/listen/bind/state/echo-gate/poll/worker-*/boundary/session-end/presence/history-log/emit/capability) + the **harness-hosted** startup topology (seed→listen→bind, interim seed-file) + local-api-auth + the generic mock adapter + contract E2E. All M2a reqs activated + green. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
  - **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). JIT plan authored when M2a lands.
  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update.
- **M4 — Networking + instances.** `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.

## De-risk spikes (do early — these can invalidate the architecture)

1. **Broker/brain no-terminate handoff** — prove a PTY child + open socket survive a daemon-*logic* restart (FD-passing or stable-broker shim). The hardest invariant in the design (ADR-0004).
2. **Iroh + mDNS smoke test** — two nodes, separate NATs, one message end-to-end; measure direct-vs-relay + binary size (research brief §5). Validates the whole networking premise.
3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.

## Immediate next steps

Progress (2026-05-31):
- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
- ✅ **Spike #1 (broker/brain handoff)** — PASS on Windows ConPTY. `docs/spikes/SPIKE-01-broker-handoff.md`. Found+fixed the ConPTY DSR-stall hazard. 4 open gaps carried to M3.
- ✅ **Spike #2 (Iroh smoke test)** — PASS (single-host). `docs/spikes/SPIKE-02-iroh-smoke.md`. iroh 0.98 API workable; 13.57 MB release. Cross-NAT/relay proof deferred to M4.
- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).

Remaining before M0:
1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).
2. **Spike #4 (manifest-driven lifecycle)** — a manifest drives one real session spawn→bind→commune→signoff. (Spike #3 ConPTY hosting is largely covered by Spike #1.)
3. Resolve the open user decisions logged in the ADR amendments (context-merge model, seed-rotation, R-UPD-3 rewording).
4. **M0** (workspace + `spt-proto` + `spt-store`) — **started**; task breakdown in [`M0-PLAN.md`](./M0-PLAN.md). Each milestone gets its own just-in-time plan (this one is M0); the roadmap stays the milestone sequence, the JIT plan is the task layer.

## Tooling: requirement traceability (adopted)

`traceable-reqs` is adopted as the requirement-tracing gate. `traceable-reqs.toml` seeds the `REQ-*` registry from PRD `R-*` + KNOWN-HAZARDS invariants (all inactive until milestone activation). The development contract + enforcement (CI gate, milestone activation, agent rule, quality audit) is `docs/TRACEABILITY.md`; the agent rule lives in `CLAUDE.md`. **M0 task:** install the CLI, validate the seed, wire the CI workflow, activate M0 reqs.

## Open threads carried forward

- New deferred items logged this session (`docs/DEFERRED.md`): block-level OCC context merge; the rebalance / Memory-Health pass; the consent framework's gated capabilities.
- The dual-audience docs strategy (`docs/DOCS-STRATEGY.md`) governs the *shipped* docs, authored as code lands.

-  - **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). JIT plan authored when M2a lands.

+  - **M2b — Live-agent lifecycle.** `spt-live` (Psyche spawn, pulse, commune/signoff drop-file ingest, echo-commune, history source). Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && git add -A && git commit -q -F - <<'EOF'
docs(M2b): author M2b-PLAN.md (live-agent lifecycle)

JIT plan for M2b — the spt-live crate executing the lifecycle seams M2a
declared+parsed. 11 tasks (T0-T10), source/req/acceptance per task + activation
map, mirroring M2a-PLAN.md.

Scope: history subsystem (3 strategies) → spawn-psyche seam (nested Psyche
perch) → echo-commune → drop-file ingest (single-writer 6.4 + precedence 6.5)
→ interim pulse → signoff/boundary (echo-before-signoff 3.3, grace 1.1, stale
sweep 3.2) → resume seam → api listen wiring → LiveAgent E2E → activation sweep.

Interim no-daemon framing carried (Psyche a real process, pulse in the listen
runner; M3 re-hosts into the daemon). Clean-room posture: copy the sister's
orphan/grace ORDERING invariants, clean-room the CC-coupled wiring behind the
M2a seams. Flags the one open gap (pulse heartbeat has no dedicated REQ —
default: mechanism under SEAM-PSYCHE, M3 daemon owns the durable loop).

Activation: SEAM-HISTORY/PSYCHE/RESUME + drop-file/signoff hazards; EP-1/2
gain live-evidence. ROADMAP M2b bullet links the plan.

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

Commit M2b plan


warning: in the working copy of 'ROADMAP.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'M2b-PLAN.md', LF will be replaced by CRLF the next time Git touches it
104ea8e docs(M2b): author M2b-PLAN.md (live-agent lifecycle)


M2b plan authored + committed (`104ea8e`).

## `M2b-PLAN.md` — what it covers

**Goal:** make a LiveAgent real — a Self + Psyche companion driven through its full lifecycle behind the M2a contract, still harness-agnostic.

**11 tasks (T0–T10):**
- T0 scaffold `spt-live` (5th layer: `…→spt-runtime→spt-live`)
- T1 history subsystem (fetcher / locate-normalize / native)
- T2 spawn-psyche seam (nested `<id>-psyche` perch via ManifestRuntime)
- T3 echo-commune (consumes history; project-primary/live-conservative)
- T4 drop-file ingest (single-writer 6.4 + precedence 6.5)
- T5 interim pulse heartbeat (in the `api listen` runner)
- T6 signoff/boundary — the ordering invariants (echo-before-signoff 3.3, grace 1.1, stale sweep 3.2)
- T7 resume seam (fresh-with-preload / continue-existing)
- T8 `api listen` lifecycle wiring
- T9 LiveAgent E2E via the mock adapter
- T10 activation sweep

**Faithful to the established discipline:**
- **Interim no-daemon** carried — Psyche a real process, pulse in the listen runner; M3 re-hosts into the daemon (notes per-pid liveness is interim-only, 2.5).
- **Clean-room posture** — copy the sister's orphan/grace *ordering* invariants (the expensive bugs), clean-room the CC-coupled wiring behind the M2a seams.
- **Maps to existing reqs** (SEAM-PSYCHE/RESUME/HISTORY + lifecycle hazards + EP-1/2) — flags the **one open gap**: the pulse heartbeat has no dedicated REQ. Default decision: treat it as interim mechanism under SEAM-PSYCHE (M3's daemon owns the durable loop), per "activate, don't pre-fail" — with option (b) to register `REQ-LIVE-PULSE` if the plan-checker wants it traced.

ROADMAP M2b bullet now links the plan. Ready to execute when you are.


<command-message>spt:commune</command-message>
<command-name>/spt:commune</command-name>
<command-args>with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session</command-args>