1	# traceable-reqs manifest for spt-core — SEED.
2	#
3	# Authoritative requirement registry: every PRD R-* requirement and every
4	# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
5	# for the development contract).
6	#
7	# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`)
8	# so `check` stays green pre-code. A milestone (M0-M5) ACTIVATES its requirements
9	# by setting their real `required_stages`. Validate this seed against the
10	# installed CLI at M0 (the binary isn't installed yet).
11
12	[scan]
13	# CONTEXT.md is a file root: the glossary is authoritative for meaning and
14	# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.
15	roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md", "docs-site", ".github"]
16
17	[policy]
18	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle
19	# reqs add "int". Until a req is activated, its per-req `required_stages = []`
20	# keeps it out of the coverage gate.
21	required_stages = []
22
23	# ───────────────────────────── Architecture / workspace ─────────────────────
24	[[requirements]]
25	id = "REQ-ARCH-1"
26	title = "Many small acyclically-layered crates"
27	required_stages = ["impl"]  # activated M0: spt-proto<-spt-store one-way layering (T14). impl-only: architecture is build-evidenced (cargo rejects cycles); grows as crates are added M1+.
28	[[requirements]]
29	id = "REQ-ARCH-2"
30	title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
31	required_stages = ["impl"]
32	[[requirements]]
33	id = "REQ-ARCH-3"
34	title = "Wire-protocol version independent of crate semver, N-1 compat window"
35	required_stages = ["impl", "unit"]  # activated M0: spt-proto wire version + compat window (T6)
36	[[requirements]]
37	id = "REQ-ARCH-4"
38	title = "Copy-verbatim the commodity layer from the sister project"
39	required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT grammar (T2)
40
41	# ───────────────────────────── Daemon ───────────────────────────────────────
42	[[requirements]]
43	id = "REQ-DAEMON-1"
44	title = "One per-machine spt-daemon owning all per-machine state"
45	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) drives the consolidated daemon-hosted lifecycle through the real IPC
46	[[requirements]]
47	id = "REQ-DAEMON-2"
48	title = "Broker/brain split for seamless self-update"
49	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves a brain kill+restart leaves the broker's PTY child + output stream intact and gapless (cross-cut with handoff.rs)
50	[[requirements]]
51	id = "REQ-DAEMON-3"
52	title = "Any api invocation auto-starts the daemon if absent"
53	required_stages = ["impl", "unit", "int"]
54	[[requirements]]
55	id = "REQ-DAEMON-4"
56	title = "Honor every KNOWN-HAZARDS invariant"
57	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves graceful signoff runs the echo-before-teardown ordering (3.3) and removes the Self's ready marker in the consolidated daemon
58
59	# ───────────────────────────── Storage ──────────────────────────────────────
60	# git-KV BranchStore substrate (ADR-0011, 2026-06-02; implement M4, optional M3c)
61	[[requirements]]
62	id = "REQ-STORE-1"
63	title = "spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)"
64	required_stages = ["impl", "unit"]  # activated M4-D6a: spt_store::branchstore::BranchStore — bare seed repo + linked worktrees (relative-path config, repair fallback), ensure_branch = parentless empty-tree seed (independent KV roots; sync joins with --allow-unrelated-histories at first contact), commit_in_worktree = ONE commit per write incl. atomic multi-key (no-op mints no empty checkpoint), tip/read_at_tip = the recovery read. Git-CLI-backed per ADR-0013 (bounded runner gitrun.rs, hazard 5.3; identity pinned via env, never user config). First consumer: contextstore.rs two-tier tracked/ layout (a-<id>/p-<project> branches) + project.rs project_id derivation (remote-URL slug → toplevel folder → dir name). unit = init-idempotent + commit/read/tip + no-op-no-commit + one-commit-multi-key + independent-roots + tier layout/commits + p-branch-aggregates-agents + invalid-id-refused + url-normalization + derivation ladder
65
66	# ───────────────────────────── Harness contract — manifest seams ────────────
67	[[requirements]]
68	id = "REQ-MANIFEST-1"
69	title = "Per-adapter manifest with adapter_name and min_spt_core_version"
70	required_stages = ["doc", "impl", "unit"]

# traceable-reqs manifest for spt-core — SEED.
#
# Authoritative requirement registry: every PRD R-* requirement and every
# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
# for the development contract).
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`)
# so `check` stays green pre-code. A milestone (M0-M5) ACTIVATES its requirements
# by setting their real `required_stages`. Validate this seed against the
# installed CLI at M0 (the binary isn't installed yet).

[scan]
# CONTEXT.md is a file root: the glossary is authoritative for meaning and
# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.
roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md", "docs-site", ".github"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle
# reqs add "int". Until a req is activated, its per-req `required_stages = []`
# keeps it out of the coverage gate.
required_stages = []

# ───────────────────────────── Architecture / workspace ─────────────────────
[[requirements]]
id = "REQ-ARCH-1"
title = "Many small acyclically-layered crates"
required_stages = ["impl"]  # activated M0: spt-proto<-spt-store one-way layering (T14). impl-only: architecture is build-evidenced (cargo rejects cycles); grows as crates are added M1+.
[[requirements]]
id = "REQ-ARCH-2"
title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
required_stages = ["impl"]
[[requirements]]
id = "REQ-ARCH-3"
title = "Wire-protocol version independent of crate semver, N-1 compat window"
required_stages = ["impl", "unit"]  # activated M0: spt-proto wire version + compat window (T6)
[[requirements]]
id = "REQ-ARCH-4"
title = "Copy-verbatim the commodity layer from the sister project"
required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT grammar (T2)

# ───────────────────────────── Daemon ───────────────────────────────────────
[[requirements]]
id = "REQ-DAEMON-1"
title = "One per-machine spt-daemon owning all per-machine state"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) drives the consolidated daemon-hosted lifecycle through the real IPC
[[requirements]]
id = "REQ-DAEMON-2"
title = "Broker/brain split for seamless self-update"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves a brain kill+restart leaves the broker's PTY child + output stream intact and gapless (cross-cut with handoff.rs)
[[requirements]]
id = "REQ-DAEMON-3"
title = "Any api invocation auto-starts the daemon if absent"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-DAEMON-4"
title = "Honor every KNOWN-HAZARDS invariant"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves graceful signoff runs the echo-before-teardown ordering (3.3) and removes the Self's ready marker in the consolidated daemon

# ───────────────────────────── Storage ──────────────────────────────────────
# git-KV BranchStore substrate (ADR-0011, 2026-06-02; implement M4, optional M3c)
[[requirements]]
id = "REQ-STORE-1"
title = "spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)"
required_stages = ["impl", "unit"]  # activated M4-D6a: spt_store::branchstore::BranchStore — bare seed repo + linked worktrees (relative-path config, repair fallback), ensure_branch = parentless empty-tree seed (independent KV roots; sync joins with --allow-unrelated-histories at first contact), commit_in_worktree = ONE commit per write incl. atomic multi-key (no-op mints no empty checkpoint), tip/read_at_tip = the recovery read. Git-CLI-backed per ADR-0013 (bounded runner gitrun.rs, hazard 5.3; identity pinned via env, never user config). First consumer: contextstore.rs two-tier tracked/ layout (a-<id>/p-<project> branches) + project.rs project_id derivation (remote-URL slug → toplevel folder → dir name). unit = init-idempotent + commit/read/tip + no-op-no-commit + one-commit-multi-key + independent-roots + tier layout/commits + p-branch-aggregates-agents + invalid-id-refused + url-normalization + derivation ladder

# ───────────────────────────── Harness contract — manifest seams ────────────
[[requirements]]
id = "REQ-MANIFEST-1"
title = "Per-adapter manifest with adapter_name and min_spt_core_version"
required_stages = ["doc", "impl", "unit"]


before ADR, reqs, or known hazards, please write a doc that details the reason for this grill session, the decisions that were made and the reasons for both. i will then have another agent verify that…


Release publish process and signing procedure


docs\adr\0001-clean-room-fork-from-claude-skill-owl.md
docs\adr\0002-bake-networking-into-core-collapse-spt-node.md
docs\adr\0003-multi-instance-node-anchored-identity-model.md
docs\adr\0004-single-daemon-broker-brain-split-and-self-update.md
docs\adr\0005-totp-seeded-spake2-node-pairing.md
docs\adr\0006-multi-subnet-membership-model.md
docs\adr\0007-subnet-notification-primitive.md
docs\adr\0008-live-activity-buffer-pty-digest.md
docs\adr\0009-endpoint-access-control-model.md
docs\adr\0010-immutable-home-subnet-fork-not-rehome.md
docs\adr\0011-branchstore-git-kv-state-substrate.md
docs\adr\0012-psyche-trust-boundary-stdout-event-outbound.md
docs\adr\0013-psyche-reconciled-context-merge.md
docs\adr\0014-publish-target-repo-and-lapse-proof-canonical-url.md
docs\adr\0015-release-signing-two-key-trust-anchor.md
docs\adr\0016-platform-targeted-update-sets.md
docs\adr\0017-subnet-mesh-membership-seed-proof.md


**Key find** — `REQ-DAEMON-2` ("Broker/brain split for seamless self-update") already exists and is marked **active + passing** with int evidence claiming *"a brain kill+restart leaves the broker's PTY child intact"* (M3b-B9). That int test runs against the **single-process** daemon — so it proves the in-process shape, **not** process-level survival. **REQ-DAEMON-2 is mis-evidenced exactly like REQ-UPD-3** — the regression is masked in the registry too. That sharpens the KNOWN-HAZARD.

Reading the remaining formats before writing:


347:id = "REQ-UPD-3"
367:id = "REQ-UPD-8"
468:id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
472:id = "REQ-HAZARD-INFO-JSON-TORN-READ"
476:id = "REQ-HAZARD-STALE-INDEX-LOCK"
480:id = "REQ-HAZARD-DEFERRED-DRAIN"
484:id = "REQ-HAZARD-WORKER-PATH"
488:id = "REQ-HAZARD-PARENT-PID-PREFER"
490:required_stages = []  # rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)
492:id = "REQ-HAZARD-STDIN-SESSION-ID"
496:id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
500:id = "REQ-HAZARD-GEN-START-NOW"
504:id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
508:id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
512:id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
516:id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
520:id = "REQ-HAZARD-ENVELOPE-CR-LINESAFE"
524:id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
528:id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
532:id = "REQ-HAZARD-ID-CHARSET"
536:id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
540:id = "REQ-HAZARD-REGISTRY-CONCURRENT"
544:id = "REQ-HAZARD-REGISTRY-DIR-CREATE"
548:id = "REQ-HAZARD-REGISTRY-EPOCH-LEASE"
552:id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
556:id = "REQ-HAZARD-INBOX-NO-DOUBLE"
560:id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
564:id = "REQ-HAZARD-EBUSY-RENAME"
568:id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
572:id = "REQ-HAZARD-UNC-PATH-STRIP"
576:id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
580:id = "REQ-HAZARD-SOFT-CLEANUP"
584:id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
588:id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
592:id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
596:id = "REQ-HAZARD-CONFLICT-BOTH-PRESERVED"
601:id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
605:id = "REQ-HAZARD-CONPTY-DSR"
609:id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
613:id = "REQ-HAZARD-INSTANT-UNDERFLOW"

[Showing results with pagination = limit: 40]


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	## De-risk spikes (do early — these can invalidate the architecture)
48
49	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).
50	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.
51	3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
52	4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.
53
54	## Immediate next steps
55
56	Progress (2026-05-31):
57	- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
58	- ✅ **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.
59	- ✅ **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.
60	- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).

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

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


Conventions captured. Notable: `REQ-HAZARD-EVENTPART-REASSEMBLY` already exists for spt-core's *own* reassembly — confirming the operator's point that the multipart-truncation bug belongs to **legacy `claude_skill_owl`**, not here, so no new spt-core hazard for it. Next ADR = **0018**. Reading the ROADMAP tail + KNOWN-HAZARDS numbering + the REQ insertion point:


60	- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).
61
62	Remaining before M0:
63	1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).
64	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.)
65	3. Resolve the open user decisions logged in the ADR amendments (context-merge model, seed-rotation, R-UPD-3 rewording).
66	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.
67
68	## Tooling: requirement traceability (adopted)
69
70	`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.
71
72	## Open threads carried forward
73
74	- 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.
75	- The dual-audience docs strategy (`docs/DOCS-STRATEGY.md`) governs the *shipped* docs, authored as code lands.
76

- ✅ **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.


340	title = "Peer-propagated update over P2P"
341	required_stages = ["impl", "unit", "int"]  # activated M4-D7-1: net/update.rs UpdRecord offer-then-fetch wire protocol + relcache.rs staged-release cache (verified-pull ⇒ staged ⇒ servable — the self-heal hop); D7-2 adds the serve/request drivers + loopback propagation E2E. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — B's staged v6 verified + staged at A under the trusted key, consent-notified)
342	[[requirements]]
343	id = "REQ-UPD-2"
344	title = "All binaries signature-verified before handoff"
345	required_stages = ["impl", "unit"]  # activated M3c-C1: release.rs Ed25519 verify-before-handoff (signature + key trust + SHA-256 artifact binding); update::plan_verified is the front door that produces a plan ONLY for a verified release, so an unverified binary never reaches apply_brain_only
346	[[requirements]]
347	id = "REQ-UPD-3"
348	title = "No endpoint process terminates/suspends during self-update"
349	required_stages = ["impl", "unit", "int"]  # activated M3c-C0: the update engine's brain-only apply swaps logic over the B2 handoff substrate; the C0 E2E (tests/update.rs) proves a live PTY child's pid is unchanged + its output stream gapless across the swap (zero endpoint interruption — ADR-0004 §A, absolute for the brain-only class)
350	[[requirements]]
351	id = "REQ-UPD-4"
352	title = "Update gated on user confirmation by default; opt-in full-auto"
353	required_stages = ["impl", "unit"]  # activated M3c-C2: consent::decide is the pure gate (default NeedsConsent, full-auto opt-in via DaemonConfig.full_auto_update); consent::most_recently_active resolves the prompt target over live perches + the new info.json last_active_ms recency stamp (set_last_active, stamped each pulse tick). Interactive prompt UX itself rides the deferred notif/PresenceChannel seam
354	[[requirements]]
355	id = "REQ-UPD-5"
356	title = "spt-core ripple-updates registered adapters"
357	required_stages = ["impl", "unit"]  # activated M3c-C3: adapter_update::conduct_ripple conducts self-then-adapters in registration order; file_pull payloads get adapter content signing (verify_signature against the per-adapter manifest key + verify_artifact digest), delegated requires a self_verifies attest
358	[[requirements]]
359	id = "REQ-UPD-6"
360	title = "Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)"
361	required_stages = ["doc", "impl", "unit", "int"]  # activated 2026-06-06: first slice = signed update-set metadata + verification, platform artifact selection, cache/propagation/apply compatibility, maintainer xtask helpers. int activated M8-D4 (decision 19): xtask debug-converge watcher per docs/DEBUG-CONVERGE-PLAN.md — status-only query on the update wire + loopback convergence test
362	[[requirements]]
363	id = "REQ-UPD-7"
364	title = "Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified."
365	required_stages = ["impl", "unit"]  # activate v0.3.1. impl = `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default latest on pinned channel); HTTPS GET of artifact + <asset>.release.json from releases/{latest,tag/<tag>}/download; feed both through plan_verified then cache.stage(); reuse cmd_update_apply machinery downstream. Add a direct HTTPS client (reqwest, blocking+rustls) on the `spt` CLI crate (fetch runs in the CLI, NOT the daemon — no new daemon HTTP surface); honor SPT_INSTALL_REPO override. unit = URL/asset-name derivation (platform × latest/tag) + verify-then-stage against a fixture release.json (reuse release.rs vectors); rollback floor + channel-pin rejection paths.
366	[[requirements]]
367	id = "REQ-UPD-8"
368	title = "Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent."
369	required_stages = ["impl", "unit"]  # activate v0.3.2. impl = fetch set-staging (cli) + apply_staged single platform-guard (applyhost) + ReleaseCache platform stamp (relcache) + product_version metadata (release.rs) + xtask release-publish emits/uploads update-set.json stamped with product_version. unit = (a) fetch update-set.json + per-asset URL/triple derivation; (b) apply REFUSES a platform-mismatched / unstamped single (the regression that bricked hfenduleam) + a matching-stamp single applies; (c) the apply message render (product_version present → "Updated spt-core to vX.Y.Z." + URL; absent → release-counter fallback).
370
371	# ───────────────────────────── Terminal wrapper / frontend ──────────────────
372	[[requirements]]
373	id = "REQ-TERM-1"
374	title = "Process-supervisor terminal wrapper hosting broker PTYs"
375	required_stages = ["impl", "unit"]
376	[[requirements]]
377	id = "REQ-TERM-2"
378	title = "session-surface abstraction; send-keys + send-line injection"
379	required_stages = ["impl", "unit"]

title = "Peer-propagated update over P2P"
required_stages = ["impl", "unit", "int"]  # activated M4-D7-1: net/update.rs UpdRecord offer-then-fetch wire protocol + relcache.rs staged-release cache (verified-pull ⇒ staged ⇒ servable — the self-heal hop); D7-2 adds the serve/request drivers + loopback propagation E2E. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — B's staged v6 verified + staged at A under the trusted key, consent-notified)
[[requirements]]
id = "REQ-UPD-2"
title = "All binaries signature-verified before handoff"
required_stages = ["impl", "unit"]  # activated M3c-C1: release.rs Ed25519 verify-before-handoff (signature + key trust + SHA-256 artifact binding); update::plan_verified is the front door that produces a plan ONLY for a verified release, so an unverified binary never reaches apply_brain_only
[[requirements]]
id = "REQ-UPD-3"
title = "No endpoint process terminates/suspends during self-update"
required_stages = ["impl", "unit", "int"]  # activated M3c-C0: the update engine's brain-only apply swaps logic over the B2 handoff substrate; the C0 E2E (tests/update.rs) proves a live PTY child's pid is unchanged + its output stream gapless across the swap (zero endpoint interruption — ADR-0004 §A, absolute for the brain-only class)
[[requirements]]
id = "REQ-UPD-4"
title = "Update gated on user confirmation by default; opt-in full-auto"
required_stages = ["impl", "unit"]  # activated M3c-C2: consent::decide is the pure gate (default NeedsConsent, full-auto opt-in via DaemonConfig.full_auto_update); consent::most_recently_active resolves the prompt target over live perches + the new info.json last_active_ms recency stamp (set_last_active, stamped each pulse tick). Interactive prompt UX itself rides the deferred notif/PresenceChannel seam
[[requirements]]
id = "REQ-UPD-5"
title = "spt-core ripple-updates registered adapters"
required_stages = ["impl", "unit"]  # activated M3c-C3: adapter_update::conduct_ripple conducts self-then-adapters in registration order; file_pull payloads get adapter content signing (verify_signature against the per-adapter manifest key + verify_artifact digest), delegated requires a self_verifies attest
[[requirements]]
id = "REQ-UPD-6"
title = "Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)"
required_stages = ["doc", "impl", "unit", "int"]  # activated 2026-06-06: first slice = signed update-set metadata + verification, platform artifact selection, cache/propagation/apply compatibility, maintainer xtask helpers. int activated M8-D4 (decision 19): xtask debug-converge watcher per docs/DEBUG-CONVERGE-PLAN.md — status-only query on the update wire + loopback convergence test
[[requirements]]
id = "REQ-UPD-7"
title = "Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified."
required_stages = ["impl", "unit"]  # activate v0.3.1. impl = `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default latest on pinned channel); HTTPS GET of artifact + <asset>.release.json from releases/{latest,tag/<tag>}/download; feed both through plan_verified then cache.stage(); reuse cmd_update_apply machinery downstream. Add a direct HTTPS client (reqwest, blocking+rustls) on the `spt` CLI crate (fetch runs in the CLI, NOT the daemon — no new daemon HTTP surface); honor SPT_INSTALL_REPO override. unit = URL/asset-name derivation (platform × latest/tag) + verify-then-stage against a fixture release.json (reuse release.rs vectors); rollback floor + channel-pin rejection paths.
[[requirements]]
id = "REQ-UPD-8"
title = "Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent."
required_stages = ["impl", "unit"]  # activate v0.3.2. impl = fetch set-staging (cli) + apply_staged single platform-guard (applyhost) + ReleaseCache platform stamp (relcache) + product_version metadata (release.rs) + xtask release-publish emits/uploads update-set.json stamped with product_version. unit = (a) fetch update-set.json + per-asset URL/triple derivation; (b) apply REFUSES a platform-mismatched / unstamped single (the regression that bricked hfenduleam) + a matching-stamp single applies; (c) the apply message render (product_version present → "Updated spt-core to vX.Y.Z." + URL; absent → release-counter fallback).

# ───────────────────────────── Terminal wrapper / frontend ──────────────────
[[requirements]]
id = "REQ-TERM-1"
title = "Process-supervisor terminal wrapper hosting broker PTYs"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-TERM-2"
title = "session-surface abstraction; send-keys + send-line injection"
required_stages = ["impl", "unit"]


608	[[requirements]]
609	id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
610	title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
611	required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)
612	[[requirements]]
613	id = "REQ-HAZARD-INSTANT-UNDERFLOW"
614	title = "Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)"
615	required_stages = ["impl", "unit"]  # activated 2026-06-07 CI failure: peer pump primed cadence legs with `Instant::now() - 86_400s`, panicking the pump thread on the sub-24h-uptime Windows runner (run 27082417706) so the subnet never converged. fix = peerloop::due(Option<Instant>) + None-seeded legs; unit asserts first-tick-due with zero instant subtraction (host-uptime-independent, the env-conditional E2E can't be the guard)
616	[[requirements]]
617	id = "REQ-HAZARD-SUDO-SECURE-PATH"
618	title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"
619	required_stages = ["impl", "unit"]  # activated 2026-06-07 field report (KITSUBITO): `spt subnet create` refused unelevated, user ran `sudo spt …` → `sudo: spt: command not found` (spt in ~/.local/bin, off secure_path). fix = elevation::rerun_command emits `sudo <abs-exe> <args>` (shell-quoted) + try_auto_elevate re-execs under sudo on interactive Unix; main.rs drops the confusing DEELEVATED line. unit = absolute-path-not-bare-name + shell-quoting + windows-none + should_auto_elevate truth table (the sudo exec itself needs a real TTY → manual/kitsubito verified)
620	[[requirements]]
621	id = "REQ-HAZARD-LOCAL-API-AUTH"
622	title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
623	required_stages = ["impl", "unit"]
624	[[requirements]]
625	id = "REQ-HAZARD-RESTART-IDEMPOTENT"
626	title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
627	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
628	[[requirements]]
629	id = "REQ-HAZARD-UPDATE-ROLLBACK"
630	title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
631	required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
632	[[requirements]]
633	id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
634	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)"
635	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
636	# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
637	[[requirements]]
638	id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
639	title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
640	required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
641	[[requirements]]
642	id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
643	title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
644	required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
645	# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
646	[[requirements]]
647	id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"

[[requirements]]
id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)
[[requirements]]
id = "REQ-HAZARD-INSTANT-UNDERFLOW"
title = "Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 CI failure: peer pump primed cadence legs with `Instant::now() - 86_400s`, panicking the pump thread on the sub-24h-uptime Windows runner (run 27082417706) so the subnet never converged. fix = peerloop::due(Option<Instant>) + None-seeded legs; unit asserts first-tick-due with zero instant subtraction (host-uptime-independent, the env-conditional E2E can't be the guard)
[[requirements]]
id = "REQ-HAZARD-SUDO-SECURE-PATH"
title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 field report (KITSUBITO): `spt subnet create` refused unelevated, user ran `sudo spt …` → `sudo: spt: command not found` (spt in ~/.local/bin, off secure_path). fix = elevation::rerun_command emits `sudo <abs-exe> <args>` (shell-quoted) + try_auto_elevate re-execs under sudo on interactive Unix; main.rs drops the confusing DEELEVATED line. unit = absolute-path-not-bare-name + shell-quoting + windows-none + should_auto_elevate truth table (the sudo exec itself needs a real TTY → manual/kitsubito verified)
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
[[requirements]]
id = "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 = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
[[requirements]]
id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
[[requirements]]
id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
[[requirements]]
id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"


Note: REQ-UPD-3's int evidence (line 349) literally says *"tests/update.rs proves a live PTY child's pid is unchanged across the swap"* — but that's the in-process brain-only apply, not a real process restart. The V5 mis-evidence is confirmed in the registry. Let me check the ROADMAP's current milestone tail for correct numbering:


5:## End goal
9:## Scope decisions (locked)
15:## Path to fruition (sequence)
25:## Milestones (all v1)
41:## Post-v1-core milestones
43:[Omitted long matching line]
45:[Omitted long matching line]
47:## De-risk spikes (do early — these can invalidate the architecture)
54:## Immediate next steps
68:## Tooling: requirement traceability (adopted)
72:## Open threads carried forward


1	# Known Hazards
2
3	Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.
4
5	**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.
6
7	---
8
9	## 1. Race conditions & ordering
10
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.
16
17	### 1.2 Poll-rewrite race & info.json mid-write reads
18	- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
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).
20	- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
21	- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.
22
23	### 1.3 Stale `index.lock` wedge from prior git crash
24	- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
25	- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
26	- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
27	- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.
28
29	### 1.4 Deferred spool rows must not leak to the event stream
30	- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.

# Known Hazards

Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.

**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.

---

## 1. Race conditions & ordering

### 1.1 Phantom INIT_SIGNOFF after grace period
- **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.
- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
- **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.
- **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.

### 1.2 Poll-rewrite race & info.json mid-write reads
- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
- **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).
- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.

### 1.3 Stale `index.lock` wedge from prior git crash
- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.

### 1.4 Deferred spool rows must not leak to the event stream
- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.


ROADMAP only goes to M7 (it's behind — M8/v0.3.x aren't recorded), so I'll add the restoration as a clearly-marked "next planned milestone" rather than force a renumber. For KNOWN-HAZARDS placement, finding the broker-handoff hazard family:


9:## 1. Race conditions & ordering
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.
43:## 2. Identity & session-binding
45:### 2.1 Parent PID over ephemeral poll PID
51:### 2.2 Stdin session_id precedence over env
57:### 2.3 Binary-handoff argv schema must stay backward-compatible
59:- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
60:- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.
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".
69:### 2.5 Daemon-hosted endpoints have no dedicated liveness PID
77:## 3. Lifecycle
99:## 4. Wire / transport
173:## 5. Platform-specific
203:- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.
244:## 6. Documented regressions (non-obvious invariants)
284:## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
294:### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
295:- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.
297:- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.
330:| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
350:| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |


244	## 6. Documented regressions (non-obvious invariants)
245
246	### 6.1 No flat/nested perch siblings; resolver-routed paths
247	- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.
248	- **Invariant:** one path resolver; never create divergent siblings.
249	- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
250	- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.
251
252	### 6.2 Soft-cleanup preserves state, removes `ready`
253	- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
254	- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
255	- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
256	- **Sister cite:** `src/owl/stop.rs`.
257
258	### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
259	- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
260	- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
261	- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
262	- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.
263
264	### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
265	- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
266	- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.
267	- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.
268	- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).
269
270	### 6.5 Direct-write precedence guard against stale LLM overwrites
271	- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
272	- **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.
273	- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity **plus a per-node version vector** (entries from each node's monotonic `EpochSource`; wall-clock never orders). Distributed rule (ADR-0013, M4-D6): dominate→accept, dominated→drop, **concurrent→surface as durable replicated conflict artifacts + Psyche-reconcile on the active instance's node — never silent newest-wins, never lose either version**. The freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the same guard read as vector dominance. Highest-value carryover for the sync design.
274	- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.
275
276	### 6.6 Surfaced context conflicts preserve both versions until dominated
277	- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.
278	- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.
279	- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.
280	- **Origin:** ADR-0013 design invariant (red-team #7's "wall-clock loses concurrent writes" closed M4-D6), not a sister bug — registered ahead of the wire path so D6c is born conformant.
281
282	---
283
284	## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
285
286	These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.
287
288	### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`

## 6. Documented regressions (non-obvious invariants)

### 6.1 No flat/nested perch siblings; resolver-routed paths
- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.
- **Invariant:** one path resolver; never create divergent siblings.
- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.

### 6.2 Soft-cleanup preserves state, removes `ready`
- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
- **Sister cite:** `src/owl/stop.rs`.

### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.

### 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.
- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).

### 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 **plus a per-node version vector** (entries from each node's monotonic `EpochSource`; wall-clock never orders). Distributed rule (ADR-0013, M4-D6): dominate→accept, dominated→drop, **concurrent→surface as durable replicated conflict artifacts + Psyche-reconcile on the active instance's node — never silent newest-wins, never lose either version**. The freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the same guard read as vector dominance. Highest-value carryover for the sync design.
- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

### 6.6 Surfaced context conflicts preserve both versions until dominated
- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.
- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.
- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.
- **Origin:** ADR-0013 design invariant (red-team #7's "wall-clock loses concurrent writes" closed M4-D6), not a sister bug — registered ahead of the wire path so D6c is born conformant.

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.

### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`


§6 ends at 6.6 — I'll add 6.7. Writing the artifacts now. First the doc §7 reattribution (legacy, not spt-core), then ADR-0018:


Edit

- **Multipart inter-agent message truncation (delivery-integrity bug, surfaced *by* this session).** During the verification exchange, an 18-part chunked message (`EVENT-PART seq=K/18`) was delivered with only ~parts 1–8; a long resend also raced/dropped its tail. Long inter-agent messages **silently lose their tail** — a delivery-integrity violation in the KH §4 envelope family (the same family as the `\r`-corruption bug that opened this whole thread). Candidate causes: chunk-reassembly drop or a per-message `EVENT-PART` cap, possibly interacting with a resident pre-restart daemon. Needs a deterministic repro (N-part message; assert reassembled byte-count == sent) + a DEFERRED/hazard row. Unrelated to the split, but must not be lost — tracked here pending operator ratification. (`doyle` can partner on a repro harness on demand.)

- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange run on `claude_skill_owl`'s transport); spt-core's *own* reassembly is already covered by `REQ-HAZARD-EVENTPART-REASSEMBLY`. It is **not an spt-core artifact** — flagged here only because the session surfaced it; route the repro + fix to the legacy project. (`doyle` can partner on a repro harness.)