1	# spt-core Roadmap
2
3	> 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.
4
5	## End goal
6
7	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.
8
9	## Scope decisions (locked)
10
11	- **v1 = the full PRD, networking included.** The networked element is the *reason* spt-core exists; it is not deferrable to v1.1.
12	- **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.
13	- **Process: lightweight but structured.** GSD is too heavy. This roadmap + the design docs are the structure.
14
15	## Path to fruition (sequence)
16
17	**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.
18
19	**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.
20
21	**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.
22
23	**Stage D — Phased build (M0–M5).** Dependency-ordered, each milestone ships something usable.
24
25	## Milestones (all v1)
26
27	- **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).
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. ✅ 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).
31	  - **M2b — Live-agent lifecycle. ✅ delivered (2026-06-01).** `spt-live` (fifth layer): history subsystem (fetcher/locate-normalize/native) + spawn-psyche (nested perch, spt-core-owned `$psyche_prompt`) + echo-commune (history brief → single-writer commune drop) + drop-file ingest (single-writer 6.4 + direct-write precedence 6.5) + interim pulse + signoff/boundary ordering invariants (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-signoff sweep 3.2) + resume seam (continue-existing / fresh-with-preload). Hosted by the `api listen` runner + `api shutdown`; mock adapter gained the lifecycle templates; LiveAgent E2E proves spawn→Psyche-perch→commune-ingest→graceful-signoff harness-agnostically. All M2b reqs activated + green. Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).
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. Split M3a/M3b/M3c. Scope + carried-forward Spike #1 gaps: [`M3-PLAN.md`](./M3-PLAN.md).
34	  - **M3a — `spt-term` session-surface mechanism. ✅ delivered (2026-06-02).** The sixth crate (`…→spt-msg→spt-term`, sibling of spt-runtime): the `SessionSurface` trait + native PTY backends (ConPTY/forkpty via `portable-pty`, REQ-TERM-1) · ConPTY-DSR auto-answer drain pump (REQ-HAZARD-CONPTY-DSR) · send-keys/send-line injection (REQ-TERM-2) · bounded byte-stream (REQ-TERM-3) · the PTY-digest parser primitive (REQ-TERM-4 impl/unit, ADR-0008). OS-neutral per Spike #4/#5 (no OS's stream contract baked in; ANSI-stripped, tail-wins repaint tolerance). Validated on Windows (ConPTY) + Linux (forkpty). All M3a reqs activated + green. Task breakdown: [`M3a-PLAN.md`](./M3a-PLAN.md).
35	  - **M3b — `spt-daemon` broker/brain process. ✅ delivered (2026-06-03).** The seventh crate (`…→spt-live→spt-daemon→spt`) hosts many `spt-term` surfaces behind a **versioned local IPC** (named-pipe/UDS behind one `DaemonTransport` trait, forward-compatible frame schema): a **broker** (stable kernel — PTY masters, harness children, sockets; no logic) + a **brain** (restartable logic) so a brain kill/restart leaves the hosted PTY child + its output stream **intact and gapless** (Spike #1 made real; `gen_start = now()` each handoff). Consolidates the interim no-daemon model — the `api listen` pulse/Psyche/drop-ingest loops fold into the brain as config-paced in-process loops (only a thin stateless relay residue) · daemon-authoritative liveness (`info.json` `status` supersedes `is_process_alive` for hosted perches) · exactly-once broker↔brain boundary (broker-owned `EffectJournal` + dedup-at-effect, Spike #6) · auto-start + in-memory seed (the seed file is dead) · supervised-crash orphan-watch (carries the M2b graceful ordering) · the **digest daemon-half** (manifest `pty_digest` seam → `DigestParser` over the broker PTY, `spt digest <id>` pull + delta-stream + opt-in Path-B persistence → REQ-TERM-4 `int`). All M3b reqs activated + green; daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) on the CI matrix (ubuntu + windows). Task breakdown: [`M3b-PLAN.md`](./M3b-PLAN.md).
36	  - **M3c — signed self-update. ✅ delivered (2026-06-03).** The gated, signed, ripple-capable self-update engine (no new crate — it lives in `spt-daemon`, signing primitives in `release.rs`): the **update-class taxonomy** (brain-only / broker-compatible / broker-breaking, ADR-0004 §A) with the **brain-only zero-interruption swap** that drives the M3b handoff substrate (snapshot→drop→re-attach) so a live PTY child + its output stream survive the logic swap **untouched** (REQ-UPD-3, proven E2E `tests/brain_swap.rs`) · **verify-before-handoff** (Ed25519 over signed release metadata + SHA-256 artifact binding; `plan_verified` is the front door — no unverified binary reaches a handoff, REQ-UPD-2) · **rollback hardening** (monotonic version, metadata expiry, channel pinning, key revocation — REQ-HAZARD-UPDATE-ROLLBACK) · **consent gating** (default-gated, delivered to the most-recently-active live session via the new `info.json` `last_active_ms` recency stamp; opt-in full-auto — REQ-UPD-4) · **adapter ripple-update** (self-then-adapters via each manifest `[update]` avenue, with adapter content signing against the adapter's own key for `file_pull` / a `self_verifies` attest for opaque `delegated` updaters — REQ-UPD-5, REQ-SEAM-UPDATE). **Peer-propagation transport (REQ-UPD-1) defers to M4** — the engine ships with self-fetch/out-of-band delivery into the same seam. All M3c reqs activated + green. Task breakdown: [`M3c-PLAN.md`](./M3c-PLAN.md).
37	  - **M3 COMPLETE** (M3a + M3b + M3c all ✅, 2026-06-03): the real ADR-0004 architecture is live — native PTY hosting, the consolidated broker/brain daemon, and seamless signed self-update. Next milestone: M4.
38	- **M4 — Networking + instances. ✅ delivered (2026-06-04).** The eighth crate `spt-net` (Iroh WAN endpoint bound to the node's own Ed25519 identity, mDNS LAN discovery, TOTP-SPAKE2 pairing over a dedicated pre-trust ALPN with transcript binding / rate limiting / seed rotation+transfer, subnet registry + NDJSON replication under the per-node epoch lease) · the multi-instance model (registry rows + bare-id resolution refuse-and-qualify + visibility/sync-membership gates + rename/collision + **resting-state machine** dormant/suspended with measured warm/cold policy [`docs/DORMANCY-BUDGET.md`, red-team #9 closed] + **resource advertisement** REQ-INST-14 + **immutable home subnet & `spt fork`** REQ-INST-15, ADR-0010) · **cross-node Psyche sync** (git-native bundles over broker QUIC streams, two-tier scope gates, vector-precedence merge with conflicts surfaced + Psyche-reconciled, ADR-0013; **the gh-repo interim is retired**) · WAN messaging + remote-drive attach + chunked file transfer + per-endpoint access whitelists · the subnet **notification primitive** (replicated spool, dismiss-replication, boundary resurfacing; update consent + escalations refactored as producers) · the **update peer-propagation transport** into the M3c engine seam (offer-then-fetch, verify-before-stage per node, consent-notified — REQ-UPD-1) · production trigger loops (peer pump + inbound dispatcher — the subnet self-drives). **The real proof ran on the real rig: `[twohost]` CI run `26958175812` — pair → register → cross-node message → remote-drive (A typed into B's live PTY and read the echo) → Psyche sync both directions → update self-heal → notif dismiss-replication, HFENDULEAM (Windows) ↔ gravity-linux, whole ladder ~8 s** (`docs/TWO-HOST-RUNBOOK.md` + evidence appendix; `tests/twohost.rs` carries the nine activated `int` stages). Task breakdowns: [`M4-PLAN.md`](./M4-PLAN.md) + per-task JIT plans (D1–D9).
39	- **M5 — Shells, presence, deferred capabilities. ✅ delivered (2026-06-04;** scope locked with user 2026-06-04, see [`M5-PLAN.md`](./M5-PLAN.md) + per-task JIT plans D0–D9). The consent framework seam (grant store + interactive escalation + pre-consent flags; remote-exec / instantiate-anywhere reserved-but-refusing) · adapter registration lifecycle (`spt adapter add/remove`, manifest-first) · **shell hosting machinery + sleep/wake** (link-token channels with per-frame MAC, broker-launched binaries, wake-watcher supervision, state-keyed wake resolution) proven by the mock **plus one real shell**: the OS-notification adapter — **standalone repo `spt-shell-notify`, zero spt-core source integration** (the manifest + binary are the whole glue; spt-core CI cross-repo hook drives its E2E) · deferred-message resting gate (REQ-INST-6) + remote `spt suspend/wake <id@node>` · **presence resolution** (the gossiped `last_active_ms` datum riding the registry rows + one most-recently-active API; notif first-fire redirects cross-node to the user's active endpoint) · the `[session.notif]` render seam generalized to shell adapters (`spt notify` → native toast) · **cross-node owner↔shell link** (relink + command drive over the wire; spawn stays same-node) · carried M4 seams closed (relay rendezvous meet routing, `spt update apply` consent-ack orchestration, self-hosted cross-runner CI). **The M5 proof ran on the real rig: `[twohost]` run `26998058816` — the full 11-rung ladder (M4's seven + remote suspend/wake, presence-shift redirect, cross-node shell relink+drive, notif→toast) green with the REAL standalone adapter rendering A's `spt notify` at B, the host the user last touched** (`docs/TWO-HOST-RUNBOOK.md` M5 evidence appendix).
40
41	## Post-v1-core milestones
42
43	- **M6 — Stage-setting: `spt-releases`, dev docs, v0.1 release. ✅ delivered (2026-06-05** — planned, executed, and shipped in one day; decisions grilled `1922fa4`, task breakdown [`M6-PLAN.md`](./M6-PLAN.md)). spt-core is now **installable, documented, and released**: [`SaberMage/spt-releases`](https://github.com/SaberMage/spt-releases) is **public** with the signed **v0.1.0** release live and GitHub Pages serving the docs at the permanent canonical URL `https://sabermage.github.io/spt-releases` (ADR-0014; repo-face README/licenses truth-synced from `releases-repo/` here). Shipped: non-interactive installer scripts (sha256-verify, user PATH, env-knob CI/air-gap override; OS-service → DEFERRED) with a staged-release CI rung on both runners · manifest **JSON Schema generated from the parsing derives themselves** + drift gate · the Tier-1 mdBook docs corpus (Starlight-styled; both killer quickstarts with real captured outputs — the messaging one CI-executed step-for-step as an anti-rot rung; complete harness-contract vertical; shell getting-started via the real `spt-shell-notify`; llms.txt/llms-full; CLI reference generated from the binary's own `--help`, drift-gated; rustdoc) · `xtask` generation/publish plumbing + tag-triggered `docs-publish` and `release` pipelines (cross-repo via fine-grained PAT; **signing stays manual + local** — `release-keygen/sign/publish/verify`, seed via password-manager CLI) · the **embedded two-key trust anchor** (ADR-0015: `rel-primary-2026` + `rel-recovery-2026` compiled into every binary, keys-file as revocable overlay; ceremony run 2026-06-05, seeds custodied off-machine) · public-facing CLI help-text polish. **Acceptance ran against the LIVE release:** the real `irm|iex` one-liner installed the published binary (`spt 0.1.0`, sha256-verified) · the published quickstarts ran as written on the released binary · llms.txt + schema + raw-`.md` + rustdoc all serve from the canonical URL · both platforms' `SignedRelease` assets **verify against the embedded anchor** (`release_verify_e2e`, REQ-REL-2 int) · a v0.1.1-shaped next release **and a recovery-key-signed metadata** both verified with the real keys (the ADR-0015 loss story, proven). Side quest: `traceable-reqs` v0.1.2 (shell/PowerShell tag scanning) shipped to cover script evidence. **Next:** M7 (subnet & quickstart UX), then the v1 acceptance proof — `spt-claude-code`, a separate downstream repo — gets its own scoping conversation.
44
45	- **M7 — Subnet & quickstart UX. ✅ delivered (2026-06-06** — planned 2026-06-05, task breakdown [`M7-PLAN.md`](./M7-PLAN.md)). Subnet setup is now a first-class, guided, secure product surface and the public quickstart is honest about personas. Shipped: the **join ceremony's product surface** (always-on daemon pairing responder + guided `spt subnet join` CLI initiator over the rig-proven SPAKE2 wire) · the `spt subnet` noun namespace (`status [NAME] [--nodes]` / `create` / `show-code` / `join`; `spt pair` deleted) · **elevation-gated membership mutations** (ADR-0005 amendment) · **node labels** (hostname-default, gossiped, addressable in `@node` qualifiers, refuse-on-ambiguity) · `spt poll` removed (`ready` + `ready --once`) · **`spt how-to <topic>`** in-binary agent guidance · quickstart rewritten as human-pairs / agents-message prompt blocks · README/docs-site sweep + `quickstart_e2e` + the twohost ladder's product-surface join rung. **Acceptance ran on REAL hardware:** fresh two-machine setup via `subnet create` + the user's own fully-guided join (elevation prompts honored), `status --nodes` labeled+online both sides, cross-machine `SENT(WAN)` both directions (HFENDULEAM ↔ gravity-linux). The run surfaced two publish-blocking field bugs, **both fixed at closeout**: elevated daemon spawn → the de-elevation seam (KNOWN-HAZARDS 5.7) and immortal ghost registry rows → silent-peer eviction (KNOWN-HAZARDS 4.10), plus refusals now rendering copy-paste qualified targets. Harvest (Linux elevation model, NTP-first clock, firewall registration at install, post-join address seeding, trust-prune verb, M8 nounification spec) → `docs/DEFERRED.md`. **Next:** `spt-claude-code` scoping (the v1 acceptance proof, own repo).
46
47	- **✅ Broker/brain process-isolation restoration — delivered (2026-06-11).** Design: [`docs/BROKER-BRAIN-SPLIT-RESTORATION.md`](./docs/BROKER-BRAIN-SPLIT-RESTORATION.md) (verified-with-amendments by `doyle`); decision: **ADR-0018**; build plan: [`RESTORATION-PLAN.md`](./RESTORATION-PLAN.md) (D1–D7). Corrects an unintended regression — the broker/brain split (ADR-0004) was specced + spiked as two *processes*, but the production daemon runs the broker as an in-process *thread* (`daemon.rs:165-170`), so `spt update apply` swaps the binary on disk yet never restarts the running code. Every release to date needed a manual fleet daemon bounce (paid 3× on v0.3.2; `enlyzeam` reproduced the `\r`-corruption for ~a day with the fix on disk). Restore the two-process model: broker = always-up per-machine anchor (seed-lock + liveness + brain supervisor) spawning the brain as a child; update = brain snapshot+self-exit → broker respawns from the swapped binary → re-attach; durable absolute-deadline loop timing; broker cursor-of-record with all-session resume; readiness-gated auto-rollback to last-known-good (two-phase applied record + the no-pre-ready-migration invariant); broker-owned generation custody; the §B-adjudicated direct-call→versioned-IPC-verb surface (net near-free, shellwake stays brain, new-brain × old-broker N-1 compat test). Re-points the regression-masked REQ-DAEMON-2 / REQ-UPD-3 `int` evidence to a productionized SPIKE-01/03 process-level survival E2E; mints `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT` (KNOWN-HAZARDS 6.7/6.8). **Sequenced before `spt-claude-code`** (operator 2026-06-09): the *last* release needing a manual bounce, so every adapter-era release rolls seamlessly and the adapter lands on the final topology; changes daemon internals only, leaving the M8-frozen CLI/api surface untouched. **Delivery (D1–D7 + field-hardening across v0.4.0–v0.4.2):** the two-process model shipped in **v0.4.0** (public 2026-06-10); the D7 process-level survival E2E (`brain_survive.rs`) + N-1 gate landed and re-pointed the regression-masked REQ-DAEMON-2/UPD-3 `int`. The **v0.4.1** fleet-verify proved Windows seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but surfaced a **Linux-only** respawn bug — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (caught by the bytes-level `exe_hash` assert; `REQ-HAZARD-BRAIN-RESPAWN-PATH` / KNOWN-HAZARDS 6.11). **v0.4.2** (public 2026-06-11) fixed it: respawn from the canonical path captured at broker start + a promotion **bytes-gate** (a wrong-bytes trial auto-rolls-back instead of recording success), with a real in-place-rename `int` E2E (`brain_respawn_rename.rs`) that fails on Linux pre-fix and is CI-gated green on both runners. **Acceptance:** Windows live-seamless proven twice (hfenduleam 0.4.0→0.4.1→0.4.2, broker pid held throughout); Linux proven by the CI-gated in-place-rename E2E on the kitsubito runner; the whole fleet (kitsubito/enlyzeam/hfenduleam) runs v0.4.2 on fixed brokers, after the project's **final two manual bounces** (kitsubito + the enlyzeam v0.3.2 catch-up). The live Linux *field*-seamless rides the next real release (operator decision 2026-06-11: close on the CI-gated E2E + fixed-broker fleet rather than a synthetic debug rollout). KNOWN-HAZARDS 6.7/6.8 closed out. **Next:** `spt-claude-code` — the v1 acceptance proof on the final topology, its own repo.
48
49	- **🔜 NEXT milestone (planned 2026-06-12) — M9: Adapter customization & session surfaces.** Product of the two-day **Gateway grill** (operator + `doyle`; CONTEXT.md mints @ `8dc22ff`/`bd469b9`; ADR-0008 amended; the lecturn device design parked in the sibling `spt-lecturn/` repo for a later build). Shell-free, independently-shippable features: **adapter profiles** (sparse leaf-replace overlays on the parent manifest; shipped + local sources, local surviving adapter updates; composite `<adapter>:<profile>` addressing; tighten-only consent floors; `create-profile`/`delete-profile`) · **adapter strings** (`[strings]` manifest KV, overlay-resolved, data-only; `get-string`/`set-string`) · **keyword hints** (`[hints]` + `spt api hint`, once-per-session, one per message — adapters teach their surface exactly-when-relevant) · **`user-msg` type** (user-authority messages, identity-gated per the KH 7.3/7.5 posture; the daemon re-stamps impostors) · **Gateway endpoint-type acceptance** (the open type system honoring a human-backed downstream type end-to-end) · **`live-role.md`** (durable broad-purpose statement in `tracked/`, injected first at start transitions, `spt endpoint role` the sole writer) · the **session-digest log-source revision** (ADR-0008 amendment: digest = a projection of normalized session logs, never PTY parsing; `api digest-entry`; REQ-TERM-4 re-point — restoration-style honest re-evidence). Task breakdown: [`M9-PLAN.md`](./M9-PLAN.md). First beneficiaries: `spt-claude-code` (profiles/hints/digest are its enablers) and the future lecturn build.
50	- **🔜 M10 — Digest extractor & thread-spanning** (planned 2026-06-13; NEXT). Product of the digest grill (operator + `doyle`); governed by **ADR-0019** (reverses M9's "no manifest seam"), CONTEXT.md "session digest" rewritten. An `spt-claude-code` enabler, so it takes the M10 slot ahead of the Shell/remote-attach work (which shift to M11/M12). Adapter-declared **`[digest]` extractor seam** (imperative, no DSL; same source files as `[history]` by default + own-source escape; `api digest-entry` push fallback; register-time validation; `spt adapter digest-proof` + runtime skip-diagnostics — no silent drop) · **thread-spanning** across `/clear` session-hops (per-endpoint session ledger + `api boundary` append + last-K enumeration + distinctive boundary marker) · **two-origin merge** (spt's context-injection entries — psyche download / echo mirror / owl message — interleaved with extracted activity by `ts`; data model only). New reqs REQ-TERM-5/6/7. Deferred: GUI collapse, echo-reads-digest loop, autonomous freshness. Task breakdown: [`DIGEST-MILESTONE-PLAN.md`](./DIGEST-MILESTONE-PLAN.md). - **Queued after M10 (each its own plan+vet cycle):** **M11 — Shell-substrate extensions** — the M5 shell mechanism already shipped; this adds the **drive channel** (owner→shell ephemeral latest-wins continuous control), the **shell tunnel** (reliable-ordered link-bound stream for opaque protocols, USB/IP-class), **per-capability approval gates** (class-keyed grants), and the **Gateway-as-owner code audit**. **M12 — Remote attach (`spt rc`)** over the already-built R-TERM substrate (controller/viewer split with controller-exclusive resize, `kick`/`--take` handoff, consent-gated input; the xterm.js bridge client = the GUI terminal pane and the future Android-gateway shape). `spt-claude-code` scoping remains on deck alongside.
51
52	> _Roadmap note: this milestone list is behind reality — M8 (CLI nounification + acceptance) and the v0.3.x release line shipped after M7 but are tracked in commits/CHANGELOG, not itemized here. A full roadmap reconciliation is a separate docs pass._
53
54	## De-risk spikes (do early — these can invalidate the architecture)
55
56	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).
57	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.
58	3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
59	4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.
60
61	## Immediate next steps
62
63	Progress (2026-05-31):
64	- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
65	- ✅ **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.
66	- ✅ **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.
67	- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).
68
69	Remaining before M0:
70	1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).
71	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.)
72	3. Resolve the open user decisions logged in the ADR amendments (context-merge model, seed-rotation, R-UPD-3 rewording).
73	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.
74
75	## Tooling: requirement traceability (adopted)
76
77	`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.
78
79	## Open threads carried forward
80
81	- **Cross-node Gateway WAN user-authority** (ratified + activated 2026-06-13). M9-T4/T5 shipped the local user-msg identity gate; the WAN half is now being completed. **Trust posture (operator-ratified):** the subnet membership boundary is the trust boundary — a subnet is machines the user already trusts, so a `user-msg` from a **Gateway-typed** origin over the subnet is honored; defending against an *in-subnet forge* of the Gateway type is explicitly out of scope (a compromised subnet member is game-over by construction). The earlier per-node "user-surface trust set" proposal was vetoed as overkill. **Work** = add `endpoint_type` to the registry `Instance` (additive, epoch-leased replication) + populate on advertise + resolve `(from-id, QUIC-proven origin-node)→endpoint_type` in `receive_wan`, flipping the already-plumbed `origin_user_backed`. Fail-closed survives only as N-1 rollout grace (a node not yet advertising `endpoint_type`). **SHIPPED 2026-06-13** on `m9-gateway-wan` (PR #9, REQ-MSG-6 doc+impl+unit) — doyle gate PASS. **REQUIRED-BEFORE-LECTURN (tracked condition from the gate):** the T5 twohost *honored-path* integration test — Gateway advert on node A → `user-msg` HONORED at B; agent advert → re-stamped — was deferred (the security-critical NEGATIVE path is already E2E-tested via the `wanmsg` funnel, the resolve logic is matrix-covered, and there is no production exerciser of the positive path until a real cross-node Gateway exists). It MUST land before `spt-lecturn` ships a real cross-node Gateway, so the honored wire path is proven before anything relies on it.
82	- 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.
83	- The dual-audience docs strategy (`docs/DOCS-STRATEGY.md`) governs the *shipped* docs, authored as code lands.
84

# 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. ✅ delivered (2026-06-01).** `spt-live` (fifth layer): history subsystem (fetcher/locate-normalize/native) + spawn-psyche (nested perch, spt-core-owned `$psyche_prompt`) + echo-commune (history brief → single-writer commune drop) + drop-file ingest (single-writer 6.4 + direct-write precedence 6.5) + interim pulse + signoff/boundary ordering invariants (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-signoff sweep 3.2) + resume seam (continue-existing / fresh-with-preload). Hosted by the `api listen` runner + `api shutdown`; mock adapter gained the lifecycle templates; LiveAgent E2E proves spawn→Psyche-perch→commune-ingest→graceful-signoff harness-agnostically. All M2b reqs activated + green. Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).
  - **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. Split M3a/M3b/M3c. Scope + carried-forward Spike #1 gaps: [`M3-PLAN.md`](./M3-PLAN.md).
  - **M3a — `spt-term` session-surface mechanism. ✅ delivered (2026-06-02).** The sixth crate (`…→spt-msg→spt-term`, sibling of spt-runtime): the `SessionSurface` trait + native PTY backends (ConPTY/forkpty via `portable-pty`, REQ-TERM-1) · ConPTY-DSR auto-answer drain pump (REQ-HAZARD-CONPTY-DSR) · send-keys/send-line injection (REQ-TERM-2) · bounded byte-stream (REQ-TERM-3) · the PTY-digest parser primitive (REQ-TERM-4 impl/unit, ADR-0008). OS-neutral per Spike #4/#5 (no OS's stream contract baked in; ANSI-stripped, tail-wins repaint tolerance). Validated on Windows (ConPTY) + Linux (forkpty). All M3a reqs activated + green. Task breakdown: [`M3a-PLAN.md`](./M3a-PLAN.md).
  - **M3b — `spt-daemon` broker/brain process. ✅ delivered (2026-06-03).** The seventh crate (`…→spt-live→spt-daemon→spt`) hosts many `spt-term` surfaces behind a **versioned local IPC** (named-pipe/UDS behind one `DaemonTransport` trait, forward-compatible frame schema): a **broker** (stable kernel — PTY masters, harness children, sockets; no logic) + a **brain** (restartable logic) so a brain kill/restart leaves the hosted PTY child + its output stream **intact and gapless** (Spike #1 made real; `gen_start = now()` each handoff). Consolidates the interim no-daemon model — the `api listen` pulse/Psyche/drop-ingest loops fold into the brain as config-paced in-process loops (only a thin stateless relay residue) · daemon-authoritative liveness (`info.json` `status` supersedes `is_process_alive` for hosted perches) · exactly-once broker↔brain boundary (broker-owned `EffectJournal` + dedup-at-effect, Spike #6) · auto-start + in-memory seed (the seed file is dead) · supervised-crash orphan-watch (carries the M2b graceful ordering) · the **digest daemon-half** (manifest `pty_digest` seam → `DigestParser` over the broker PTY, `spt digest <id>` pull + delta-stream + opt-in Path-B persistence → REQ-TERM-4 `int`). All M3b reqs activated + green; daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) on the CI matrix (ubuntu + windows). Task breakdown: [`M3b-PLAN.md`](./M3b-PLAN.md).
  - **M3c — signed self-update. ✅ delivered (2026-06-03).** The gated, signed, ripple-capable self-update engine (no new crate — it lives in `spt-daemon`, signing primitives in `release.rs`): the **update-class taxonomy** (brain-only / broker-compatible / broker-breaking, ADR-0004 §A) with the **brain-only zero-interruption swap** that drives the M3b handoff substrate (snapshot→drop→re-attach) so a live PTY child + its output stream survive the logic swap **untouched** (REQ-UPD-3, proven E2E `tests/brain_swap.rs`) · **verify-before-handoff** (Ed25519 over signed release metadata + SHA-256 artifact binding; `plan_verified` is the front door — no unverified binary reaches a handoff, REQ-UPD-2) · **rollback hardening** (monotonic version, metadata expiry, channel pinning, key revocation — REQ-HAZARD-UPDATE-ROLLBACK) · **consent gating** (default-gated, delivered to the most-recently-active live session via the new `info.json` `last_active_ms` recency stamp; opt-in full-auto — REQ-UPD-4) · **adapter ripple-update** (self-then-adapters via each manifest `[update]` avenue, with adapter content signing against the adapter's own key for `file_pull` / a `self_verifies` attest for opaque `delegated` updaters — REQ-UPD-5, REQ-SEAM-UPDATE). **Peer-propagation transport (REQ-UPD-1) defers to M4** — the engine ships with self-fetch/out-of-band delivery into the same seam. All M3c reqs activated + green. Task breakdown: [`M3c-PLAN.md`](./M3c-PLAN.md).
  - **M3 COMPLETE** (M3a + M3b + M3c all ✅, 2026-06-03): the real ADR-0004 architecture is live — native PTY hosting, the consolidated broker/brain daemon, and seamless signed self-update. Next milestone: M4.
- **M4 — Networking + instances. ✅ delivered (2026-06-04).** The eighth crate `spt-net` (Iroh WAN endpoint bound to the node's own Ed25519 identity, mDNS LAN discovery, TOTP-SPAKE2 pairing over a dedicated pre-trust ALPN with transcript binding / rate limiting / seed rotation+transfer, subnet registry + NDJSON replication under the per-node epoch lease) · the multi-instance model (registry rows + bare-id resolution refuse-and-qualify + visibility/sync-membership gates + rename/collision + **resting-state machine** dormant/suspended with measured warm/cold policy [`docs/DORMANCY-BUDGET.md`, red-team #9 closed] + **resource advertisement** REQ-INST-14 + **immutable home subnet & `spt fork`** REQ-INST-15, ADR-0010) · **cross-node Psyche sync** (git-native bundles over broker QUIC streams, two-tier scope gates, vector-precedence merge with conflicts surfaced + Psyche-reconciled, ADR-0013; **the gh-repo interim is retired**) · WAN messaging + remote-drive attach + chunked file transfer + per-endpoint access whitelists · the subnet **notification primitive** (replicated spool, dismiss-replication, boundary resurfacing; update consent + escalations refactored as producers) · the **update peer-propagation transport** into the M3c engine seam (offer-then-fetch, verify-before-stage per node, consent-notified — REQ-UPD-1) · production trigger loops (peer pump + inbound dispatcher — the subnet self-drives). **The real proof ran on the real rig: `[twohost]` CI run `26958175812` — pair → register → cross-node message → remote-drive (A typed into B's live PTY and read the echo) → Psyche sync both directions → update self-heal → notif dismiss-replication, HFENDULEAM (Windows) ↔ gravity-linux, whole ladder ~8 s** (`docs/TWO-HOST-RUNBOOK.md` + evidence appendix; `tests/twohost.rs` carries the nine activated `int` stages). Task breakdowns: [`M4-PLAN.md`](./M4-PLAN.md) + per-task JIT plans (D1–D9).
- **M5 — Shells, presence, deferred capabilities. ✅ delivered (2026-06-04;** scope locked with user 2026-06-04, see [`M5-PLAN.md`](./M5-PLAN.md) + per-task JIT plans D0–D9). The consent framework seam (grant store + interactive escalation + pre-consent flags; remote-exec / instantiate-anywhere reserved-but-refusing) · adapter registration lifecycle (`spt adapter add/remove`, manifest-first) · **shell hosting machinery + sleep/wake** (link-token channels with per-frame MAC, broker-launched binaries, wake-watcher supervision, state-keyed wake resolution) proven by the mock **plus one real shell**: the OS-notification adapter — **standalone repo `spt-shell-notify`, zero spt-core source integration** (the manifest + binary are the whole glue; spt-core CI cross-repo hook drives its E2E) · deferred-message resting gate (REQ-INST-6) + remote `spt suspend/wake <id@node>` · **presence resolution** (the gossiped `last_active_ms` datum riding the registry rows + one most-recently-active API; notif first-fire redirects cross-node to the user's active endpoint) · the `[session.notif]` render seam generalized to shell adapters (`spt notify` → native toast) · **cross-node owner↔shell link** (relink + command drive over the wire; spawn stays same-node) · carried M4 seams closed (relay rendezvous meet routing, `spt update apply` consent-ack orchestration, self-hosted cross-runner CI). **The M5 proof ran on the real rig: `[twohost]` run `26998058816` — the full 11-rung ladder (M4's seven + remote suspend/wake, presence-shift redirect, cross-node shell relink+drive, notif→toast) green with the REAL standalone adapter rendering A's `spt notify` at B, the host the user last touched** (`docs/TWO-HOST-RUNBOOK.md` M5 evidence appendix).

## Post-v1-core milestones

- **M6 — Stage-setting: `spt-releases`, dev docs, v0.1 release. ✅ delivered (2026-06-05** — planned, executed, and shipped in one day; decisions grilled `1922fa4`, task breakdown [`M6-PLAN.md`](./M6-PLAN.md)). spt-core is now **installable, documented, and released**: [`SaberMage/spt-releases`](https://github.com/SaberMage/spt-releases) is **public** with the signed **v0.1.0** release live and GitHub Pages serving the docs at the permanent canonical URL `https://sabermage.github.io/spt-releases` (ADR-0014; repo-face README/licenses truth-synced from `releases-repo/` here). Shipped: non-interactive installer scripts (sha256-verify, user PATH, env-knob CI/air-gap override; OS-service → DEFERRED) with a staged-release CI rung on both runners · manifest **JSON Schema generated from the parsing derives themselves** + drift gate · the Tier-1 mdBook docs corpus (Starlight-styled; both killer quickstarts with real captured outputs — the messaging one CI-executed step-for-step as an anti-rot rung; complete harness-contract vertical; shell getting-started via the real `spt-shell-notify`; llms.txt/llms-full; CLI reference generated from the binary's own `--help`, drift-gated; rustdoc) · `xtask` generation/publish plumbing + tag-triggered `docs-publish` and `release` pipelines (cross-repo via fine-grained PAT; **signing stays manual + local** — `release-keygen/sign/publish/verify`, seed via password-manager CLI) · the **embedded two-key trust anchor** (ADR-0015: `rel-primary-2026` + `rel-recovery-2026` compiled into every binary, keys-file as revocable overlay; ceremony run 2026-06-05, seeds custodied off-machine) · public-facing CLI help-text polish. **Acceptance ran against the LIVE release:** the real `irm|iex` one-liner installed the published binary (`spt 0.1.0`, sha256-verified) · the published quickstarts ran as written on the released binary · llms.txt + schema + raw-`.md` + rustdoc all serve from the canonical URL · both platforms' `SignedRelease` assets **verify against the embedded anchor** (`release_verify_e2e`, REQ-REL-2 int) · a v0.1.1-shaped next release **and a recovery-key-signed metadata** both verified with the real keys (the ADR-0015 loss story, proven). Side quest: `traceable-reqs` v0.1.2 (shell/PowerShell tag scanning) shipped to cover script evidence. **Next:** M7 (subnet & quickstart UX), then the v1 acceptance proof — `spt-claude-code`, a separate downstream repo — gets its own scoping conversation.

- **M7 — Subnet & quickstart UX. ✅ delivered (2026-06-06** — planned 2026-06-05, task breakdown [`M7-PLAN.md`](./M7-PLAN.md)). Subnet setup is now a first-class, guided, secure product surface and the public quickstart is honest about personas. Shipped: the **join ceremony's product surface** (always-on daemon pairing responder + guided `spt subnet join` CLI initiator over the rig-proven SPAKE2 wire) · the `spt subnet` noun namespace (`status [NAME] [--nodes]` / `create` / `show-code` / `join`; `spt pair` deleted) · **elevation-gated membership mutations** (ADR-0005 amendment) · **node labels** (hostname-default, gossiped, addressable in `@node` qualifiers, refuse-on-ambiguity) · `spt poll` removed (`ready` + `ready --once`) · **`spt how-to <topic>`** in-binary agent guidance · quickstart rewritten as human-pairs / agents-message prompt blocks · README/docs-site sweep + `quickstart_e2e` + the twohost ladder's product-surface join rung. **Acceptance ran on REAL hardware:** fresh two-machine setup via `subnet create` + the user's own fully-guided join (elevation prompts honored), `status --nodes` labeled+online both sides, cross-machine `SENT(WAN)` both directions (HFENDULEAM ↔ gravity-linux). The run surfaced two publish-blocking field bugs, **both fixed at closeout**: elevated daemon spawn → the de-elevation seam (KNOWN-HAZARDS 5.7) and immortal ghost registry rows → silent-peer eviction (KNOWN-HAZARDS 4.10), plus refusals now rendering copy-paste qualified targets. Harvest (Linux elevation model, NTP-first clock, firewall registration at install, post-join address seeding, trust-prune verb, M8 nounification spec) → `docs/DEFERRED.md`. **Next:** `spt-claude-code` scoping (the v1 acceptance proof, own repo).

- **✅ Broker/brain process-isolation restoration — delivered (2026-06-11).** Design: [`docs/BROKER-BRAIN-SPLIT-RESTORATION.md`](./docs/BROKER-BRAIN-SPLIT-RESTORATION.md) (verified-with-amendments by `doyle`); decision: **ADR-0018**; build plan: [`RESTORATION-PLAN.md`](./RESTORATION-PLAN.md) (D1–D7). Corrects an unintended regression — the broker/brain split (ADR-0004) was specced + spiked as two *processes*, but the production daemon runs the broker as an in-process *thread* (`daemon.rs:165-170`), so `spt update apply` swaps the binary on disk yet never restarts the running code. Every release to date needed a manual fleet daemon bounce (paid 3× on v0.3.2; `enlyzeam` reproduced the `\r`-corruption for ~a day with the fix on disk). Restore the two-process model: broker = always-up per-machine anchor (seed-lock + liveness + brain supervisor) spawning the brain as a child; update = brain snapshot+self-exit → broker respawns from the swapped binary → re-attach; durable absolute-deadline loop timing; broker cursor-of-record with all-session resume; readiness-gated auto-rollback to last-known-good (two-phase applied record + the no-pre-ready-migration invariant); broker-owned generation custody; the §B-adjudicated direct-call→versioned-IPC-verb surface (net near-free, shellwake stays brain, new-brain × old-broker N-1 compat test). Re-points the regression-masked REQ-DAEMON-2 / REQ-UPD-3 `int` evidence to a productionized SPIKE-01/03 process-level survival E2E; mints `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT` (KNOWN-HAZARDS 6.7/6.8). **Sequenced before `spt-claude-code`** (operator 2026-06-09): the *last* release needing a manual bounce, so every adapter-era release rolls seamlessly and the adapter lands on the final topology; changes daemon internals only, leaving the M8-frozen CLI/api surface untouched. **Delivery (D1–D7 + field-hardening across v0.4.0–v0.4.2):** the two-process model shipped in **v0.4.0** (public 2026-06-10); the D7 process-level survival E2E (`brain_survive.rs`) + N-1 gate landed and re-pointed the regression-masked REQ-DAEMON-2/UPD-3 `int`. The **v0.4.1** fleet-verify proved Windows seamless (hfenduleam: brain pid rolls, broker held, `exe_hash` flips, no manual bounce) but surfaced a **Linux-only** respawn bug — the resident broker respawned the brain via per-spawn `current_exe()`, which on Linux follows the `apply` rename to `.old-N` and ran OLD bytes under an `applied` record (caught by the bytes-level `exe_hash` assert; `REQ-HAZARD-BRAIN-RESPAWN-PATH` / KNOWN-HAZARDS 6.11). **v0.4.2** (public 2026-06-11) fixed it: respawn from the canonical path captured at broker start + a promotion **bytes-gate** (a wrong-bytes trial auto-rolls-back instead of recording success), with a real in-place-rename `int` E2E (`brain_respawn_rename.rs`) that fails on Linux pre-fix and is CI-gated green on both runners. **Acceptance:** Windows live-seamless proven twice (hfenduleam 0.4.0→0.4.1→0.4.2, broker pid held throughout); Linux proven by the CI-gated in-place-rename E2E on the kitsubito runner; the whole fleet (kitsubito/enlyzeam/hfenduleam) runs v0.4.2 on fixed brokers, after the project's **final two manual bounces** (kitsubito + the enlyzeam v0.3.2 catch-up). The live Linux *field*-seamless rides the next real release (operator decision 2026-06-11: close on the CI-gated E2E + fixed-broker fleet rather than a synthetic debug rollout). KNOWN-HAZARDS 6.7/6.8 closed out. **Next:** `spt-claude-code` — the v1 acceptance proof on the final topology, its own repo.

- **🔜 NEXT milestone (planned 2026-06-12) — M9: Adapter customization & session surfaces.** Product of the two-day **Gateway grill** (operator + `doyle`; CONTEXT.md mints @ `8dc22ff`/`bd469b9`; ADR-0008 amended; the lecturn device design parked in the sibling `spt-lecturn/` repo for a later build). Shell-free, independently-shippable features: **adapter profiles** (sparse leaf-replace overlays on the parent manifest; shipped + local sources, local surviving adapter updates; composite `<adapter>:<profile>` addressing; tighten-only consent floors; `create-profile`/`delete-profile`) · **adapter strings** (`[strings]` manifest KV, overlay-resolved, data-only; `get-string`/`set-string`) · **keyword hints** (`[hints]` + `spt api hint`, once-per-session, one per message — adapters teach their surface exactly-when-relevant) · **`user-msg` type** (user-authority messages, identity-gated per the KH 7.3/7.5 posture; the daemon re-stamps impostors) · **Gateway endpoint-type acceptance** (the open type system honoring a human-backed downstream type end-to-end) · **`live-role.md`** (durable broad-purpose statement in `tracked/`, injected first at start transitions, `spt endpoint role` the sole writer) · the **session-digest log-source revision** (ADR-0008 amendment: digest = a projection of normalized session logs, never PTY parsing; `api digest-entry`; REQ-TERM-4 re-point — restoration-style honest re-evidence). Task breakdown: [`M9-PLAN.md`](./M9-PLAN.md). First beneficiaries: `spt-claude-code` (profiles/hints/digest are its enablers) and the future lecturn build.
- **🔜 M10 — Digest extractor & thread-spanning** (planned 2026-06-13; NEXT). Product of the digest grill (operator + `doyle`); governed by **ADR-0019** (reverses M9's "no manifest seam"), CONTEXT.md "session digest" rewritten. An `spt-claude-code` enabler, so it takes the M10 slot ahead of the Shell/remote-attach work (which shift to M11/M12). Adapter-declared **`[digest]` extractor seam** (imperative, no DSL; same source files as `[history]` by default + own-source escape; `api digest-entry` push fallback; register-time validation; `spt adapter digest-proof` + runtime skip-diagnostics — no silent drop) · **thread-spanning** across `/clear` session-hops (per-endpoint session ledger + `api boundary` append + last-K enumeration + distinctive boundary marker) · **two-origin merge** (spt's context-injection entries — psyche download / echo mirror / owl message — interleaved with extracted activity by `ts`; data model only). New reqs REQ-TERM-5/6/7. Deferred: GUI collapse, echo-reads-digest loop, autonomous freshness. Task breakdown: [`DIGEST-MILESTONE-PLAN.md`](./DIGEST-MILESTONE-PLAN.md). - **Queued after M10 (each its own plan+vet cycle):** **M11 — Shell-substrate extensions** — the M5 shell mechanism already shipped; this adds the **drive channel** (owner→shell ephemeral latest-wins continuous control), the **shell tunnel** (reliable-ordered link-bound stream for opaque protocols, USB/IP-class), **per-capability approval gates** (class-keyed grants), and the **Gateway-as-owner code audit**. **M12 — Remote attach (`spt rc`)** over the already-built R-TERM substrate (controller/viewer split with controller-exclusive resize, `kick`/`--take` handoff, consent-gated input; the xterm.js bridge client = the GUI terminal pane and the future Android-gateway shape). `spt-claude-code` scoping remains on deck alongside.

> _Roadmap note: this milestone list is behind reality — M8 (CLI nounification + acceptance) and the v0.3.x release line shipped after M7 but are tracked in commits/CHANGELOG, not itemized here. A full roadmap reconciliation is a separate docs pass._

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

- **Cross-node Gateway WAN user-authority** (ratified + activated 2026-06-13). M9-T4/T5 shipped the local user-msg identity gate; the WAN half is now being completed. **Trust posture (operator-ratified):** the subnet membership boundary is the trust boundary — a subnet is machines the user already trusts, so a `user-msg` from a **Gateway-typed** origin over the subnet is honored; defending against an *in-subnet forge* of the Gateway type is explicitly out of scope (a compromised subnet member is game-over by construction). The earlier per-node "user-surface trust set" proposal was vetoed as overkill. **Work** = add `endpoint_type` to the registry `Instance` (additive, epoch-leased replication) + populate on advertise + resolve `(from-id, QUIC-proven origin-node)→endpoint_type` in `receive_wan`, flipping the already-plumbed `origin_user_backed`. Fail-closed survives only as N-1 rollout grace (a node not yet advertising `endpoint_type`). **SHIPPED 2026-06-13** on `m9-gateway-wan` (PR #9, REQ-MSG-6 doc+impl+unit) — doyle gate PASS. **REQUIRED-BEFORE-LECTURN (tracked condition from the gate):** the T5 twohost *honored-path* integration test — Gateway advert on node A → `user-msg` HONORED at B; agent advert → re-stamped — was deferred (the security-critical NEGATIVE path is already E2E-tested via the `wanmsg` funnel, the resolve logic is matrix-covered, and there is no production exerciser of the positive path until a real cross-node Gateway exists). It MUST land before `spt-lecturn` ships a real cross-node Gateway, so the honored wire path is proven before anything relies on it.
- 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.


doyle ack received. Let me orient on the active JIT plan before picking up the queue.


Bash

ls *PLAN*.md docs/*PLAN*.md 2>/dev/null; echo "---"; ls v0.13* V0.13* 2>/dev/null; echo "---recent plans---"; git log --oneline -5 --diff-filter=A -- '*PLAN*'

Find active JIT plan files


CI-SELFHOST-PLAN.md
DAEMON-LIFECYCLE-COMPLETION-PLAN.md
DAEMON-LIFECYCLE-PLAN.md
DIGEST-MILESTONE-PLAN.md
M0-PLAN.md
M1-PLAN.md
M11-PLAN.md
M12-PLAN.md
M12-W1-PLAN.md
M12-W1.5-PLAN.md
M12-W2-PLAN.md
M12-W2.5-PLAN.md
M12-W3-PLAN.md
M12-W4-PLAN.md
M12-W5-PLAN.md
M2a-PLAN.md
M2b-PLAN.md
M3-PLAN.md
M3a-PLAN.md
M3b-PLAN.md
M3c-PLAN.md
M4-D2-WIRE-PLAN.md
M4-D2f-PLAN.md
M4-D2g-PLAN.md
M4-D3-PLAN.md
M4-D4-PLAN.md
M4-D5-PLAN.md
M4-D6-PLAN.md
M4-D7-PLAN.md
M4-D7.5-PLAN.md
M4-D8-PLAN.md
M4-D9-PLAN.md
M4-PLAN.md
M5-D3-PLAN.md
M5-D4-PLAN.md
M5-D5-PLAN.md
M5-D6-PLAN.md
M5-D7-PLAN.md
M5-D8-PLAN.md
M5-D9-PLAN.md
M5-PLAN.md
M6-PLAN.md
M7-PLAN.md
M8-PLAN.md
M9-GATEWAY-WAN-PLAN.md
M9-PLAN.md
MESH-D0-PLAN.md
MESH-D1-PLAN.md
MESH-D2-PLAN.md
MESH-D3-PLAN.md
MESH-D4-PLAN.md
MESH-D5-PLAN.md
MESH-D6-PLAN.md
MESH-D7-PLAN.md
MESH-D8-PLAN.md
PUMP-SEAM-PLAN.md
REQ-MSG-ENVELOPE-PLAN.md
RESTORATION-D1-PLAN.md
RESTORATION-D2-PLAN.md
RESTORATION-D3-PLAN.md
RESTORATION-D4-PLAN.md
RESTORATION-D5-PLAN.md
RESTORATION-D6-PLAN.md
RESTORATION-D7-PLAN.md
RESTORATION-PLAN.md
SUBNET-MESH-PLAN.md
V031-PLAN.md
V032-PLAN.md
V042-PLAN.md
V074-PLAN.md
docs/DEBUG-CONVERGE-PLAN.md
---
V0.13.0-DELIVERY-CONTROL-JIT.md
---recent plans---
e6722b8 plan: v0.7.4 build (gh_release update avenue + adapter-binary install-dir resolution)
f1620df docs(plan): M11-W0 — daemon-hosted live-agent lifecycle completion plan
5251d16 M11-T0: shell-ownership audit + register substrate-extension reqs
24e0628 docs(adr): ADR-0020 — <EVENT> envelope is the sole arriving-message format; __REPLY_TO__ removed
6b4859f feat(rc,broker): M12 W2.5 — controller/viewer remote-attach model + loud Kick


1	# v0.13.0 — spt-hosted message delivery + the control/wedge bug cluster (JIT plan)
2
3	> Milestone born from operator dogfooding of v0.12.1/v0.12.2 (grill-with-docs, 2026-06-19).
4	> Scope decision: **one milestone** — folds the in-flight v0.12.2 work (window-regression fix +
5	> Markdown-output sweep, already gated on branch `v0.12.2-cli-fixes`) into v0.13.0.
6	> **Binding lesson (v0.12.0/v0.12.1):** several "fixes" shipped green-but-broken because they were
7	> gated on theory / mocks, not the REAL claude-spt harness. EVERY fix here is gated against a REAL
8	> dummy-harness fixture + real daemon. No prove-don't-change shortcuts on a hazard that recurred.
9
10	## The unifying invariant (the spine)
11
12	**The broker must accept INJECTED keystrokes (the v0.11.0 raw direct-inject today; the
13	manifest-declared translation-binary tomorrow) AND a live `spt rc` controller into the SAME
14	broker-held PTY without (a) the operator losing control, (b) the endpoint latching
15	`ONLINE+CONTROLLED`, or (c) the broker wedging.** The injection inlet is permanent —
16	spt-claude-code *requires* keystroke injection — so this is root-caused + fixed at the
17	PTY-injection layer, IN STEP with the delivery redesign that formalizes the inlet.
18
19	## Recovered design (CCS session 16531b26, 2026-06-17 — Q1→"A+" locked, rest deferred)
20
21	Idle message delivery to spt-hosted endpoints, redesigned:
22	- **`[message-idle-translation-binary]`** — opt-in adapter manifest key. **= the binary PATH**
23	  (operator: *binary owns the choreography*). spt-core **manages its lifecycle** (up with the
24	  endpoint, down when the endpoint goes down).
25	- The binary (i) **hosts the poll listener + subscribes to its output**, (ii) **drives parsed
26	  output + keystrokes into the broker PTY** via an spt-core **send-keys API** supporting
27	  **arbitrary keys + pre/post delays**. CC choreography (operator-specified):
28	  `ctrl+s (stash) → 50ms → payload + \r → 50ms → ctrl+s (restore)`.
29	- **Unified substrate (Q1=A):** the daemon's **poll feed** is the ONE idle substrate for both
30	  topologies; consumer differs — harness-hosted = the Monitor child; spt-hosted = the translation
31	  binary. **spt-core prefers the poll listener for a perch IF it exists** (so spt-hosted can use a
32	  listener AND keep `spt rc`). Idle-only; **busy = hook-injection** (adapter, mid-turn).
33	- Today's hardcoded direct-PTY-inject (payload+`\r`, no choreography) = the **degenerate special
34	  case** of a translation-binary — and the cause of the control-loss (it fights the controller).
35	- **Grounding gap (current):** `api bind` registers NO listener port → a listener-less spt-hosted
36	  perch SPOOLS inbound messages (only spooling+adapter-poll works today). The redesign closes this.
37
38	### RESOLVED (grilled 2026-06-19 → **ADR-0022**). Final contract:
39	- **`[message-idle-translation-binary]`** = scalar binary path, opt-in per manifest. NEW primitive
40	  (not collapsed into `[inject]`/`notif_command`; shares the poll-feed substrate).
41	- Binary = **pure stdin→stdout filter**; **spt-core owns lifecycle + every PTY write**.
42	- **stdin** (spt-core→binary, JSON-lines): `{type:"init",endpoint_id,node}` first ·
43	  `{type:"event",envelope:"<EVENT…>"}` per message · `{type:"input"}` content-free ping on operator
44	  keystroke (binary tracks user-idle for its own idle-gated buffering).
45	- **stdout** (binary→spt-core, JSON-lines): `{key:…}` · `{delay_ms:…}` · `{text:…}`.
46	- spt-core applies the sequence to the PTY **ATOMICALLY** — controller input buffered during the
47	  sequence, flushed after (so ctrl+s stash/restore can't be clobbered). This is the
48	  injection-coexists-with-controller mechanism (shared with W1).
49	- spt-core PREFERS a perch's poll listener if present. Idle-only; busy = adapter hook-injection.
50	- v0.11.0 raw inject = degenerate `{text:payload}{key:enter}` case.
51	- CONTEXT.md:39 reworded (W0); busy/idle split per ADR-0022.
52
53	## Waves
54
55	### W0 — Design formalization (doyle; gate: operator sign-off)
56	- Resolve the deferred forks above. Author **ADR-0022 (spt-hosted idle delivery redesign)** — it
57	  qualifies (hard-to-reverse delivery-model change; surprising = "why a separate binary"; real
58	  trade-off = binary-owns vs manifest-declares, chose binary). Reword CONTEXT.md:39 +
59	  delivery section. Add the `[message-idle-translation-binary]` row to the inject-seam family.
60	- Mint the milestone REQs (registry-first, stages=[] until each wave builds).
61
62	### W1 — KEYSTONE: broker-never-wedges + control-loss root cause (todlando build, doyle gate)
63	- **Repro FIRST** on the REAL dummy-harness fixture: an `spt rc`-attached spt-hosted session,
64	  inject a message (keystrokes) → instrument to find the EXACT blocking call (operator's signal:
65	  one keystroke succeeds, the next wedges → single-threaded broker parks on a blocking PTY/loopback
66	  write after injection-induced output). Do NOT fix on theory — nail the mechanism.
67	- Fix the data-plane: injected keystrokes + controller input + PTY-output drain must never
68	  deadlock the broker (candidates: non-blocking/fail-fast PTY write, split input/output, bounded-
69	  evicting). **REOPEN REQ-HAZARD-ATTACH-WEDGE** (v0.12.1 prove-don't-change was insufficient — the
70	  wedge recurs via the injection trigger, NOT just dead-child).
71	- int gate: the repro fixture asserts — after injection, the controller keeps control, no wedge,
72	  a new attach still serves, `daemon stop` bounded.
73
74	### W1b — PRIORITY: the effect-journal PTY-input wedge (W1 escape; todlando build, doyle gate)
75	- **W1 escape, found in operator dogfood 2026-06-19.** W1 fixed the OUTPUT drain (real). The
76	  INPUT/effect-journal path is the untouched wedge. REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE.
77	- ROOT (measured): `apply_once` (effect.rs:168-188) holds one global mutex across PENDING-fsync →
78	  `effect()` (the blocking PtyWrite) → DONE-fsync; `write_line` does sync_all() twice per effect.
79	  (A) every keystroke = 2 fsync serialized (measured 6.5ms median / 198ms spike) → stutter; (B) a
80	  blocked PtyWrite holds the lock forever → dispatch can't open attaches → `--view/--take` →
81	  'brain IPC read deadline elapsed' (permanent). Refutes the W2-deferred 'ConPTY-benign' ruling.
82	- This IS W2 Layer C's substrate (own-every-PTY-write-atomically + the park bound) — build it as
83	  the FOUNDATION of Layer C, before the translation choreography rides on top.
84	- Fix >= (1) don't hold the lock across effect() + (3) drop per-keystroke fsync (PtyWrite is
85	  ephemeral; in-memory applied-set dedup suffices, broker-survives-brain is the anchor); keep
86	  fsync for durable kinds. (2) bound/fail-fast PtyWrite (the deferred Unix park bound).
87	- int gate: extend inject_control_wedge.rs to a REAL backed-up PTY consumer + a REAL rc-client
88	  attach that stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked
89	  (it only checked KIND_SESSIONS liveness, never real attach delivery -> how this escaped).
90
91	#### W1b BUILD STATUS (todlando, in progress) — PRIORITY pivot from W2
92	- PIVOTED from W2 Layer C per doyle (this IS Layer C's substrate). W2 banked clean: A@c73eb6a,
93	  B@611205c, D-plumbing@7166f52, Layer-C InjectFloor kernel@79f5a40 (sits ON TOP of this fix).
94	- ROOT (confirmed in effect.rs): `apply_once` holds `inner.lock()` across `write_line(PENDING)`
95	  (fsync) -> `effect()` (the blocking PtyWrite) -> `write_line(DONE)` (fsync). Lines 168-188;
96	  `write_line` 235-239 = flush()+sync_all().
97	- FIX (mine, impl) — (1)+(3) minimum, doyle-gated: add `EffectKind::is_durable()` (PtyWrite=false,
98	  others=true). `apply_once`: reserve key (+ fsync PENDING for DURABLE) under lock -> RELEASE lock
99	  -> run effect() OFF lock -> re-acquire -> finalize (mark applied; + fsync DONE for durable). A
100	  concurrent in-flight same-key (pending) re-drive dedups. PtyWrite = NO journal lines / NO fsync
101	  (in-memory dedup only; ephemeral — lost keystroke retyped, broker-survives-brain is the anchor).
102	  Durable kinds (NetSend/NetDial/Registry/Spool) keep exact current behavior.
103	- FIX (2) park bound (pty.rs write_input holds writer mutex across blocking write_all; DSR-answer
104	  off the mutex) — Unix-forkpty only (Windows ConPTY absorbs = benign), gate gravity-linux. doyle:
105	  minimum is (1)+(3); (2) rides if clean else gravity follow-up within W1b.
106	- TESTS via spt-test-engineer (dispatched @a4f3a273, RED-capture-first): structural units in
107	  effect.rs (barrier proves lock-not-held-across-effect; PtyWrite writes NO journal line vs NetSend
108	  does = fsync proxy) + int in inject_control_wedge.rs (real broker+PTY, stalled consumer, concurrent
109	  attach STILL serviced + real byte receipt; Unix park-(b) folded; cfg-split Windows-benign).
110	  doyle GATE-GUARD: structural NOT fsync wall-clock (flaky). REPRO must be RED pre-fix.
111	- ORDER (blocking): subagent captures RED on UNFIXED code -> THEN I apply effect.rs fix (effect.rs +
112	  pty.rs are OFF-LIMITS until RED captured, else the repro can't reproduce) -> re-run GREEN -> activate
113	  REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE [impl,unit,int] + KNOWN-HAZARDS entry -> doyle gate -> resume W2 C.
114
115	### W2 — KEYSTONE: delivery redesign (todlando build, doyle gate; depends W0)
116	- `[message-idle-translation-binary]` manifest key + validate; spt-core lifecycle-manages the
117	  binary (up/down with endpoint). The send-keys API (arbitrary keys + delays). Poll-feed substrate
118	  + listener-preference rule. spt-hosted gets real inbound delivery (closes the spool-only gap).
119	- int gate: a real dummy translation-binary delivers an inbound message into an rc-attached
120	  spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant).
121
122	#### W2 BUILD STATUS (todlando, in progress) — locked decisions + layer ledger
123	**Contract decisions (doyle-gated, BINDING):**
124	- **Manifest shape = TABLE** `[message-idle-translation-binary]` with a `path` scalar
125	  (doyle OPT-B). NOT a bare top-level scalar (silent-absorb footgun). spt-core does
126	  NOT `deny_unknown_fields` → future keys degrade gracefully. Known keys: `path`.
127	- **Atomic-sequence boundary = explicit `{commit:true}` terminator** (doyle OPT-1).
128	  The binary emits it after the last keystroke of its response to an `event`.
129	- **`{commit}` is PER-EVENT**: each `{type:event}` dispatch OPENS the inject floor
130	  (buffers controller input), its matching `{commit}` CLOSES it (flush buffer →
131	  release floor). Concurrent events QUEUE — never interleave two uncommitted
132	  sequences. `{type:input}` pings keep flowing to the binary during buffering (idle
133	  tracking); the operator's real keystrokes are the BUFFERED bytes.
134	- **MANDATORY BOUNDED BUFFER (doyle, non-negotiable, = W1 'never park forever'):**
135	  `INJECT_COMMIT_DEADLINE` (~5s, reuse W1's order). No `{commit}` within deadline →
136	  flush buffer + release floor + log warning + FAULT the binary (terminate, do NOT
137	  respawn → degrade to plain pass-through, NEVER a dead operator). MUST be gated by
138	  a test: an event sequence WITHOUT commit must not wedge controller input.
139	- **doyle gate reminders (assert at int):** (1) `{type:input}` ping is CONTENT-FREE
140	  — no PTY input content leaks to the binary's stdin (privacy). (2) lifecycle kill
141	  on session-down is BOUNDED + reaped — no zombie translation child.
142
143	**Layer ledger:**
144	- A. Manifest table key + validate + schema regen — **DONE @c73eb6a** (REQ doc stage).
145	- B. `spt-daemon/translation.rs` — ToBinary(init/event/input) + KeyCmd(key/text/
146	  delay_ms/commit, untagged) + `key_to_bytes` send-keys map + `TranslationChild`
147	  (piped stdin, stdout-reader thread→Sender<KeyCmd>, Drop=bounded reap 500ms) —
148	  **DONE @611205c**. 3 unit tests (wire round-trips, content-free ping, send-keys).
149	- D-plumbing. `SpawnReq.translation_binary: Option<String>` (additive) threaded from
150	  manifest via harnesshost — **IN PROGRESS** (field added to msg.rs; ~25 struct-
151	  literal sites swept to `translation_binary: None`; then harnesshost/
152	  launch_harness_brokered_in fills it from `manifest.message_idle_translation_binary
153	  .path`).
154	- C. **NEXT** — broker atomic apply: per-session inject floor (state) + controller-
155	  input buffer in `dispatch_input`; on `{type:event}` open floor & feed binary, drain
156	  `KeyCmd`s via an applier thread (Receiver from TranslationChild), apply key/text→
157	  bytes via `session.write_input` with `delay_ms` sleeps, `{commit}` closes floor &
158	  flushes buffered controller bytes; `INJECT_COMMIT_DEADLINE` guard. Reuses W1's
159	  off-drain writer. Wire `{type:input}` ping emission from `dispatch_input`.
160	- E. Routing: `dispatch_endpoint_input` — if a translation child is attached, send
161	  `{type:event,envelope}` to it instead of raw inject; raw inject = degenerate
162	  fallback when no binary. Poll-listener preference rule.
163	- F. Unix park (b)/(c) bound: bounded/fail-fast `write_input` + DSR-answer must not
164	  hold the writer mutex across a blocking write (spt-term/pty.rs); gate gravity-linux
165	  (cfg(unix) park-(b) assert in inject_control_wedge.rs).
166	- G. int gate: REAL dummy translation-binary fixture → delivers inbound into an
167	  rc-attached spt-hosted session via ctrl+s→delay→text→enter→delay→ctrl+s→commit
168	  choreography WITHOUT breaking control; + no-commit-deadline test; + no-zombie +
169	  content-free assertions. Author via spt-test-engineer. doyle gates here.
170	- Tests authored via spt-test-engineer subagent. REQ stages bump impl/unit/int as
171	  layers land; activate full set before declaring W2 done. traceable-reqs check + CI
172	  workspace clippy (`cargo clippy --workspace --all-targets -- -D warnings`) + real
173	  bin rebuild (`cargo build -p spt --bin spt`) at the W2 gate.
174
175	### W3 — P1: fresh endpoint shows under its project (todlando build, doyle gate)
176	- ROOT (found): `info.cwd` is NEVER set on bind. Thread cwd into `establish_perch` + set `rec.cwd`:
177	  `cmd_bind` (spt-hosted) reads its own `current_dir` (the broker spawned it in `project_cwd`);
178	  `bind_from_seed` (harness-hosted) passes `seed.cwd` (already captured, currently discarded).
179	- int gate: a REAL `endpoint run` perch has `info.cwd` set AND appears under its project tab
180	  (the v0.12.1 unit tested merge_origin_project with a *provided* origin — never asserted cwd is set).
181
182	### W4 — Picker UX (todlando build, doyle gate)
183	- Skip the first screen → open directly on **Pick existing**; `n` jumps to Create new.
184	- **Auto-attach** after Start-new AND Resume-from-history (both currently don't attach + show no
185	  stdout); add an `h` shortcut to run headless (no attach).
186	- `controlled by` shows the **node NAME** (node_label_display), not the raw hex (D).
187	- Clean up the Start-new output: drop the Rust `pid=Some(142748)` leak + the "harness binds its
188	  perch on startup" internals; user-friendly, not process-log.
189
190	### W5 — `driven_by` self-heal (todlando build, doyle gate)
191	- `ONLINE+CONTROLLED` must clear even if a detach is lost (don't rely on the detach IPC — same
192	  lesson as B2/REQ-HAZARD-HOSTED-LIVENESS-RECONCILE): reconcile clears `driven_by` when the
193	  endpoint has no live controller/session. Composes with W1 (wedge no longer blocks detach).
194
195	### W6 — perri hand-off (doyle, after W2)
196	- Direct perri to the UPDATED integration checklist + docs to build the spt-claude-code
197	  **translation binary** (CC choreography: ctrl+s→50ms→payload+\r→50ms→ctrl+s). Per the standing
198	  route: doyle designs (W0 ADR) → perri builds the adapter binary → doyle gates → perri validates.
199
200	### Side task (do early, docs-only) — correct v0.12.1 changelog
201	- v0.12.1 notes claim "a freshly-created agent appears under its own project right away" — REFUTED
202	  (P1 never worked). Amend CHANGELOG.md (main) + `gh release edit v0.12.1` notes, no retag
203	  (v0.12.0-precedent). v0.13.0 delivers it for real (W3).
204
205	## Sequencing notes
206	- W0 gates W2 (design before build). W1 (wedge/control) is independent + the scariest — can start
207	  immediately (repro-first). W3/W4/W5 are independent bug/UX fixes, parallelizable. W2+W6 are the
208	  feature spine. The folded v0.12.2 work (window fix @15fdf58, MD-sweep @2435a74, ATX @b3bb387)
209	  rides the v0.13.0 release; its changelog bullets merge in.
210	- Version: **v0.13.0** (MINOR — new delivery feature + breaking-ish behavior). Confirm at release.
211

# v0.13.0 — spt-hosted message delivery + the control/wedge bug cluster (JIT plan)

> Milestone born from operator dogfooding of v0.12.1/v0.12.2 (grill-with-docs, 2026-06-19).
> Scope decision: **one milestone** — folds the in-flight v0.12.2 work (window-regression fix +
> Markdown-output sweep, already gated on branch `v0.12.2-cli-fixes`) into v0.13.0.
> **Binding lesson (v0.12.0/v0.12.1):** several "fixes" shipped green-but-broken because they were
> gated on theory / mocks, not the REAL claude-spt harness. EVERY fix here is gated against a REAL
> dummy-harness fixture + real daemon. No prove-don't-change shortcuts on a hazard that recurred.

## The unifying invariant (the spine)

**The broker must accept INJECTED keystrokes (the v0.11.0 raw direct-inject today; the
manifest-declared translation-binary tomorrow) AND a live `spt rc` controller into the SAME
broker-held PTY without (a) the operator losing control, (b) the endpoint latching
`ONLINE+CONTROLLED`, or (c) the broker wedging.** The injection inlet is permanent —
spt-claude-code *requires* keystroke injection — so this is root-caused + fixed at the
PTY-injection layer, IN STEP with the delivery redesign that formalizes the inlet.

## Recovered design (CCS session 16531b26, 2026-06-17 — Q1→"A+" locked, rest deferred)

Idle message delivery to spt-hosted endpoints, redesigned:
- **`[message-idle-translation-binary]`** — opt-in adapter manifest key. **= the binary PATH**
  (operator: *binary owns the choreography*). spt-core **manages its lifecycle** (up with the
  endpoint, down when the endpoint goes down).
- The binary (i) **hosts the poll listener + subscribes to its output**, (ii) **drives parsed
  output + keystrokes into the broker PTY** via an spt-core **send-keys API** supporting
  **arbitrary keys + pre/post delays**. CC choreography (operator-specified):
  `ctrl+s (stash) → 50ms → payload + \r → 50ms → ctrl+s (restore)`.
- **Unified substrate (Q1=A):** the daemon's **poll feed** is the ONE idle substrate for both
  topologies; consumer differs — harness-hosted = the Monitor child; spt-hosted = the translation
  binary. **spt-core prefers the poll listener for a perch IF it exists** (so spt-hosted can use a
  listener AND keep `spt rc`). Idle-only; **busy = hook-injection** (adapter, mid-turn).
- Today's hardcoded direct-PTY-inject (payload+`\r`, no choreography) = the **degenerate special
  case** of a translation-binary — and the cause of the control-loss (it fights the controller).
- **Grounding gap (current):** `api bind` registers NO listener port → a listener-less spt-hosted
  perch SPOOLS inbound messages (only spooling+adapter-poll works today). The redesign closes this.

### RESOLVED (grilled 2026-06-19 → **ADR-0022**). Final contract:
- **`[message-idle-translation-binary]`** = scalar binary path, opt-in per manifest. NEW primitive
  (not collapsed into `[inject]`/`notif_command`; shares the poll-feed substrate).
- Binary = **pure stdin→stdout filter**; **spt-core owns lifecycle + every PTY write**.
- **stdin** (spt-core→binary, JSON-lines): `{type:"init",endpoint_id,node}` first ·
  `{type:"event",envelope:"<EVENT…>"}` per message · `{type:"input"}` content-free ping on operator
  keystroke (binary tracks user-idle for its own idle-gated buffering).
- **stdout** (binary→spt-core, JSON-lines): `{key:…}` · `{delay_ms:…}` · `{text:…}`.
- spt-core applies the sequence to the PTY **ATOMICALLY** — controller input buffered during the
  sequence, flushed after (so ctrl+s stash/restore can't be clobbered). This is the
  injection-coexists-with-controller mechanism (shared with W1).
- spt-core PREFERS a perch's poll listener if present. Idle-only; busy = adapter hook-injection.
- v0.11.0 raw inject = degenerate `{text:payload}{key:enter}` case.
- CONTEXT.md:39 reworded (W0); busy/idle split per ADR-0022.

## Waves

### W0 — Design formalization (doyle; gate: operator sign-off)
- Resolve the deferred forks above. Author **ADR-0022 (spt-hosted idle delivery redesign)** — it
  qualifies (hard-to-reverse delivery-model change; surprising = "why a separate binary"; real
  trade-off = binary-owns vs manifest-declares, chose binary). Reword CONTEXT.md:39 +
  delivery section. Add the `[message-idle-translation-binary]` row to the inject-seam family.
- Mint the milestone REQs (registry-first, stages=[] until each wave builds).

### W1 — KEYSTONE: broker-never-wedges + control-loss root cause (todlando build, doyle gate)
- **Repro FIRST** on the REAL dummy-harness fixture: an `spt rc`-attached spt-hosted session,
  inject a message (keystrokes) → instrument to find the EXACT blocking call (operator's signal:
  one keystroke succeeds, the next wedges → single-threaded broker parks on a blocking PTY/loopback
  write after injection-induced output). Do NOT fix on theory — nail the mechanism.
- Fix the data-plane: injected keystrokes + controller input + PTY-output drain must never
  deadlock the broker (candidates: non-blocking/fail-fast PTY write, split input/output, bounded-
  evicting). **REOPEN REQ-HAZARD-ATTACH-WEDGE** (v0.12.1 prove-don't-change was insufficient — the
  wedge recurs via the injection trigger, NOT just dead-child).
- int gate: the repro fixture asserts — after injection, the controller keeps control, no wedge,
  a new attach still serves, `daemon stop` bounded.

### W1b — PRIORITY: the effect-journal PTY-input wedge (W1 escape; todlando build, doyle gate)
- **W1 escape, found in operator dogfood 2026-06-19.** W1 fixed the OUTPUT drain (real). The
  INPUT/effect-journal path is the untouched wedge. REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE.
- ROOT (measured): `apply_once` (effect.rs:168-188) holds one global mutex across PENDING-fsync →
  `effect()` (the blocking PtyWrite) → DONE-fsync; `write_line` does sync_all() twice per effect.
  (A) every keystroke = 2 fsync serialized (measured 6.5ms median / 198ms spike) → stutter; (B) a
  blocked PtyWrite holds the lock forever → dispatch can't open attaches → `--view/--take` →
  'brain IPC read deadline elapsed' (permanent). Refutes the W2-deferred 'ConPTY-benign' ruling.
- This IS W2 Layer C's substrate (own-every-PTY-write-atomically + the park bound) — build it as
  the FOUNDATION of Layer C, before the translation choreography rides on top.
- Fix >= (1) don't hold the lock across effect() + (3) drop per-keystroke fsync (PtyWrite is
  ephemeral; in-memory applied-set dedup suffices, broker-survives-brain is the anchor); keep
  fsync for durable kinds. (2) bound/fail-fast PtyWrite (the deferred Unix park bound).
- int gate: extend inject_control_wedge.rs to a REAL backed-up PTY consumer + a REAL rc-client
  attach that stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked
  (it only checked KIND_SESSIONS liveness, never real attach delivery -> how this escaped).

#### W1b BUILD STATUS (todlando, in progress) — PRIORITY pivot from W2
- PIVOTED from W2 Layer C per doyle (this IS Layer C's substrate). W2 banked clean: A@c73eb6a,
  B@611205c, D-plumbing@7166f52, Layer-C InjectFloor kernel@79f5a40 (sits ON TOP of this fix).
- ROOT (confirmed in effect.rs): `apply_once` holds `inner.lock()` across `write_line(PENDING)`
  (fsync) -> `effect()` (the blocking PtyWrite) -> `write_line(DONE)` (fsync). Lines 168-188;
  `write_line` 235-239 = flush()+sync_all().
- FIX (mine, impl) — (1)+(3) minimum, doyle-gated: add `EffectKind::is_durable()` (PtyWrite=false,
  others=true). `apply_once`: reserve key (+ fsync PENDING for DURABLE) under lock -> RELEASE lock
  -> run effect() OFF lock -> re-acquire -> finalize (mark applied; + fsync DONE for durable). A
  concurrent in-flight same-key (pending) re-drive dedups. PtyWrite = NO journal lines / NO fsync
  (in-memory dedup only; ephemeral — lost keystroke retyped, broker-survives-brain is the anchor).
  Durable kinds (NetSend/NetDial/Registry/Spool) keep exact current behavior.
- FIX (2) park bound (pty.rs write_input holds writer mutex across blocking write_all; DSR-answer
  off the mutex) — Unix-forkpty only (Windows ConPTY absorbs = benign), gate gravity-linux. doyle:
  minimum is (1)+(3); (2) rides if clean else gravity follow-up within W1b.
- TESTS via spt-test-engineer (dispatched @a4f3a273, RED-capture-first): structural units in
  effect.rs (barrier proves lock-not-held-across-effect; PtyWrite writes NO journal line vs NetSend
  does = fsync proxy) + int in inject_control_wedge.rs (real broker+PTY, stalled consumer, concurrent
  attach STILL serviced + real byte receipt; Unix park-(b) folded; cfg-split Windows-benign).
  doyle GATE-GUARD: structural NOT fsync wall-clock (flaky). REPRO must be RED pre-fix.
- ORDER (blocking): subagent captures RED on UNFIXED code -> THEN I apply effect.rs fix (effect.rs +
  pty.rs are OFF-LIMITS until RED captured, else the repro can't reproduce) -> re-run GREEN -> activate
  REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE [impl,unit,int] + KNOWN-HAZARDS entry -> doyle gate -> resume W2 C.

### W2 — KEYSTONE: delivery redesign (todlando build, doyle gate; depends W0)
- `[message-idle-translation-binary]` manifest key + validate; spt-core lifecycle-manages the
  binary (up/down with endpoint). The send-keys API (arbitrary keys + delays). Poll-feed substrate
  + listener-preference rule. spt-hosted gets real inbound delivery (closes the spool-only gap).
- int gate: a real dummy translation-binary delivers an inbound message into an rc-attached
  spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant).

#### W2 BUILD STATUS (todlando, in progress) — locked decisions + layer ledger
**Contract decisions (doyle-gated, BINDING):**
- **Manifest shape = TABLE** `[message-idle-translation-binary]` with a `path` scalar
  (doyle OPT-B). NOT a bare top-level scalar (silent-absorb footgun). spt-core does
  NOT `deny_unknown_fields` → future keys degrade gracefully. Known keys: `path`.
- **Atomic-sequence boundary = explicit `{commit:true}` terminator** (doyle OPT-1).
  The binary emits it after the last keystroke of its response to an `event`.
- **`{commit}` is PER-EVENT**: each `{type:event}` dispatch OPENS the inject floor
  (buffers controller input), its matching `{commit}` CLOSES it (flush buffer →
  release floor). Concurrent events QUEUE — never interleave two uncommitted
  sequences. `{type:input}` pings keep flowing to the binary during buffering (idle
  tracking); the operator's real keystrokes are the BUFFERED bytes.
- **MANDATORY BOUNDED BUFFER (doyle, non-negotiable, = W1 'never park forever'):**
  `INJECT_COMMIT_DEADLINE` (~5s, reuse W1's order). No `{commit}` within deadline →
  flush buffer + release floor + log warning + FAULT the binary (terminate, do NOT
  respawn → degrade to plain pass-through, NEVER a dead operator). MUST be gated by
  a test: an event sequence WITHOUT commit must not wedge controller input.
- **doyle gate reminders (assert at int):** (1) `{type:input}` ping is CONTENT-FREE
  — no PTY input content leaks to the binary's stdin (privacy). (2) lifecycle kill
  on session-down is BOUNDED + reaped — no zombie translation child.

**Layer ledger:**
- A. Manifest table key + validate + schema regen — **DONE @c73eb6a** (REQ doc stage).
- B. `spt-daemon/translation.rs` — ToBinary(init/event/input) + KeyCmd(key/text/
  delay_ms/commit, untagged) + `key_to_bytes` send-keys map + `TranslationChild`
  (piped stdin, stdout-reader thread→Sender<KeyCmd>, Drop=bounded reap 500ms) —
  **DONE @611205c**. 3 unit tests (wire round-trips, content-free ping, send-keys).
- D-plumbing. `SpawnReq.translation_binary: Option<String>` (additive) threaded from
  manifest via harnesshost — **IN PROGRESS** (field added to msg.rs; ~25 struct-
  literal sites swept to `translation_binary: None`; then harnesshost/
  launch_harness_brokered_in fills it from `manifest.message_idle_translation_binary
  .path`).
- C. **NEXT** — broker atomic apply: per-session inject floor (state) + controller-
  input buffer in `dispatch_input`; on `{type:event}` open floor & feed binary, drain
  `KeyCmd`s via an applier thread (Receiver from TranslationChild), apply key/text→
  bytes via `session.write_input` with `delay_ms` sleeps, `{commit}` closes floor &
  flushes buffered controller bytes; `INJECT_COMMIT_DEADLINE` guard. Reuses W1's
  off-drain writer. Wire `{type:input}` ping emission from `dispatch_input`.
- E. Routing: `dispatch_endpoint_input` — if a translation child is attached, send
  `{type:event,envelope}` to it instead of raw inject; raw inject = degenerate
  fallback when no binary. Poll-listener preference rule.
- F. Unix park (b)/(c) bound: bounded/fail-fast `write_input` + DSR-answer must not
  hold the writer mutex across a blocking write (spt-term/pty.rs); gate gravity-linux
  (cfg(unix) park-(b) assert in inject_control_wedge.rs).
- G. int gate: REAL dummy translation-binary fixture → delivers inbound into an
  rc-attached spt-hosted session via ctrl+s→delay→text→enter→delay→ctrl+s→commit
  choreography WITHOUT breaking control; + no-commit-deadline test; + no-zombie +
  content-free assertions. Author via spt-test-engineer. doyle gates here.
- Tests authored via spt-test-engineer subagent. REQ stages bump impl/unit/int as
  layers land; activate full set before declaring W2 done. traceable-reqs check + CI
  workspace clippy (`cargo clippy --workspace --all-targets -- -D warnings`) + real
  bin rebuild (`cargo build -p spt --bin spt`) at the W2 gate.

### W3 — P1: fresh endpoint shows under its project (todlando build, doyle gate)
- ROOT (found): `info.cwd` is NEVER set on bind. Thread cwd into `establish_perch` + set `rec.cwd`:
  `cmd_bind` (spt-hosted) reads its own `current_dir` (the broker spawned it in `project_cwd`);
  `bind_from_seed` (harness-hosted) passes `seed.cwd` (already captured, currently discarded).
- int gate: a REAL `endpoint run` perch has `info.cwd` set AND appears under its project tab
  (the v0.12.1 unit tested merge_origin_project with a *provided* origin — never asserted cwd is set).

### W4 — Picker UX (todlando build, doyle gate)
- Skip the first screen → open directly on **Pick existing**; `n` jumps to Create new.
- **Auto-attach** after Start-new AND Resume-from-history (both currently don't attach + show no
  stdout); add an `h` shortcut to run headless (no attach).
- `controlled by` shows the **node NAME** (node_label_display), not the raw hex (D).
- Clean up the Start-new output: drop the Rust `pid=Some(142748)` leak + the "harness binds its
  perch on startup" internals; user-friendly, not process-log.

### W5 — `driven_by` self-heal (todlando build, doyle gate)
- `ONLINE+CONTROLLED` must clear even if a detach is lost (don't rely on the detach IPC — same
  lesson as B2/REQ-HAZARD-HOSTED-LIVENESS-RECONCILE): reconcile clears `driven_by` when the
  endpoint has no live controller/session. Composes with W1 (wedge no longer blocks detach).

### W6 — perri hand-off (doyle, after W2)
- Direct perri to the UPDATED integration checklist + docs to build the spt-claude-code
  **translation binary** (CC choreography: ctrl+s→50ms→payload+\r→50ms→ctrl+s). Per the standing
  route: doyle designs (W0 ADR) → perri builds the adapter binary → doyle gates → perri validates.

### Side task (do early, docs-only) — correct v0.12.1 changelog
- v0.12.1 notes claim "a freshly-created agent appears under its own project right away" — REFUTED
  (P1 never worked). Amend CHANGELOG.md (main) + `gh release edit v0.12.1` notes, no retag
  (v0.12.0-precedent). v0.13.0 delivers it for real (W3).

## Sequencing notes
- W0 gates W2 (design before build). W1 (wedge/control) is independent + the scariest — can start
  immediately (repro-first). W3/W4/W5 are independent bug/UX fixes, parallelizable. W2+W6 are the
  feature spine. The folded v0.12.2 work (window fix @15fdf58, MD-sweep @2435a74, ATX @b3bb387)
  rides the v0.13.0 release; its changelog bullets merge in.
- Version: **v0.13.0** (MINOR — new delivery feature + breaking-ish behavior). Confirm at release.


doyle's queue: (1) W7-swap (2) F1 test-hang guard (3) W2 Layer C proper. Need context on W7-swap + F1 — not in JIT. Check psyche snapshot + grep.


Read full psyche snapshot