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	roots = ["src", "tests", "docs", "crates"]
14
15	[policy]
16	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle
17	# reqs add "int". Until a req is activated, its per-req `required_stages = []`
18	# keeps it out of the coverage gate.
19	required_stages = []
20
21	# ───────────────────────────── Architecture / workspace ─────────────────────
22	[[requirements]]
23	id = "REQ-ARCH-1"
24	title = "Many small acyclically-layered crates"
25	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+.
26	[[requirements]]
27	id = "REQ-ARCH-2"
28	title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
29	required_stages = ["impl"]
30	[[requirements]]
31	id = "REQ-ARCH-3"
32	title = "Wire-protocol version independent of crate semver, N-1 compat window"
33	required_stages = ["impl", "unit"]  # activated M0: spt-proto wire version + compat window (T6)
34	[[requirements]]
35	id = "REQ-ARCH-4"
36	title = "Copy-verbatim the commodity layer from the sister project"
37	required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT grammar (T2)
38
39	# ───────────────────────────── Daemon ───────────────────────────────────────
40	[[requirements]]
41	id = "REQ-DAEMON-1"
42	title = "One per-machine spt-daemon owning all per-machine state"
43	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
44	[[requirements]]
45	id = "REQ-DAEMON-2"
46	title = "Broker/brain split for seamless self-update"
47	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)
48	[[requirements]]
49	id = "REQ-DAEMON-3"
50	title = "Any api invocation auto-starts the daemon if absent"
51	required_stages = ["impl", "unit", "int"]
52	[[requirements]]
53	id = "REQ-DAEMON-4"
54	title = "Honor every KNOWN-HAZARDS invariant"
55	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
56
57	# ───────────────────────────── Storage ──────────────────────────────────────
58	# git-KV BranchStore substrate (ADR-0011, 2026-06-02; implement M4, optional M3c)
59	[[requirements]]
60	id = "REQ-STORE-1"
61	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)"
62	required_stages = []
63
64	# ───────────────────────────── Harness contract — manifest seams ────────────
65	[[requirements]]
66	id = "REQ-MANIFEST-1"
67	title = "Per-adapter manifest with adapter_name and min_spt_core_version"
68	required_stages = ["doc", "impl", "unit"]
69	[[requirements]]
70	id = "REQ-SEAM-SPAWN"
71	title = "spawn-session seam"
72	required_stages = ["impl", "unit"]
73	[[requirements]]
74	id = "REQ-SEAM-POSTSPAWN"
75	title = "post-spawn / api bind seam with boot nonce"
76	required_stages = ["impl", "unit"]
77	[[requirements]]
78	id = "REQ-SEAM-PSYCHE"
79	title = "spawn-psyche seam (fresh + resume templates)"
80	required_stages = ["impl", "unit", "int"]
81	[[requirements]]
82	id = "REQ-SEAM-HISTORY"
83	title = "History subsystem (fetcher / locate-normalize / native store)"
84	required_stages = ["impl", "unit", "int"]
85	[[requirements]]
86	id = "REQ-SEAM-ACTIVITY"
87	title = "Activity/idle reported via api sentinels, not PTY quiescence"
88	required_stages = ["impl", "unit"]
89	[[requirements]]
90	id = "REQ-SEAM-INJECT"
91	title = "inject-input methods configurable per activity-state"
92	required_stages = ["impl", "unit"]
93	[[requirements]]
94	id = "REQ-SEAM-RESUME"
95	title = "resume-session seam (fresh-with-preload / continue-existing)"
96	required_stages = ["impl", "unit"]
97	[[requirements]]
98	id = "REQ-SEAM-CAPABILITY"
99	title = "Hostable endpoint-types capability declaration"
100	required_stages = ["impl", "unit"]
101	[[requirements]]
102	id = "REQ-SEAM-UPDATE"
103	title = "Adapter-update avenue (file-pull / delegated command)"
104	required_stages = ["impl", "unit"]  # activated M3c-C3: manifest [update] gains signing_key (file_pull content key) + self_verifies (delegated attest); adapter_update::plan_adapter_update dispatches each avenue (file_pull verified against the adapter key, delegated delegated-or-skipped)
105
106	# ───────────────────────────── Harness contract — api surface ───────────────
107	[[requirements]]
108	id = "REQ-API-1"
109	title = "api prefix and adapter_name on every machinery invocation"
110	required_stages = ["impl", "unit", "int"]
111	[[requirements]]
112	id = "REQ-API-2"
113	title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
114	required_stages = ["impl", "unit", "int"]
115	[[requirements]]
116	id = "REQ-API-3"
117	title = "commune/signoff are file-drops, not commands"
118	required_stages = ["impl", "unit", "int"]
119
120	# ───────────────────────────── Startup flows ────────────────────────────────
121	[[requirements]]
122	id = "REQ-START-1"
123	title = "Adapters never resolve SPT_HOME; binary on PATH; api bridging only"
124	required_stages = ["impl", "unit"]
125	[[requirements]]
126	id = "REQ-START-2"
127	title = "Harness-hosted startup: api seed then listen"
128	required_stages = ["impl", "unit", "int"]
129	[[requirements]]
130	id = "REQ-START-3"
131	title = "spt-hosted startup: spawn-session then api bind (no file)"
132	required_stages = ["impl", "unit", "int"]
133	[[requirements]]
134	id = "REQ-START-4"
135	title = "Adapter-injected env aliases (SPT/OWL/LIVE)"
136	required_stages = ["impl", "unit"]
137
138	# ───────────────────────────── Endpoints / Shells ───────────────────────────
139	[[requirements]]
140	id = "REQ-EP-1"
141	title = "Day-one endpoint types; open type system"
142	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
143	[[requirements]]
144	id = "REQ-EP-2"
145	title = "Agent endpoints vs Shells distinction in the type model"
146	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
147	[[requirements]]
148	id = "REQ-EP-3"
149	title = "Messaging payloads carry typed operation commands + file blobs"
150	required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
151	[[requirements]]
152	id = "REQ-EP-4"
153	title = "PresenceChannel broker endpoint (seam day-one)"
154	required_stages = []
155	[[requirements]]
156	id = "REQ-EP-5"
157	title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
158	required_stages = []
159
160	# ───────────────────────────── Instances ────────────────────────────────────
161	[[requirements]]
162	id = "REQ-INST-1"
163	title = "endpoint ID vs instance split (adapter-agnostic ID)"
164	required_stages = []
165	[[requirements]]
166	id = "REQ-INST-2"
167	title = "Per-node files, synced Psyche mind"
168	required_stages = []
169	[[requirements]]
170	id = "REQ-INST-3"
171	title = "Dormant (warm) / suspended (cold) resting states"
172	required_stages = []
173	[[requirements]]
174	id = "REQ-INST-4"
175	title = "active to dormant/suspended fires a transition echo commune"
176	required_stages = []
177	[[requirements]]
178	id = "REQ-INST-5"
179	title = "Two-tier context sync (live to all, project to same-project)"
180	required_stages = []
181	[[requirements]]
182	id = "REQ-INST-6"
183	title = "Deferred messages not delivered to dormant/suspended instances"
184	required_stages = []
185	[[requirements]]
186	id = "REQ-INST-7"
187	title = "Subnet registry + bare-id resolution policy"
188	required_stages = ["impl", "unit"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10); cross-node replication = D4; int two-host = D9. unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes.
189	[[requirements]]
190	id = "REQ-INST-8"
191	title = "Remote-control mode distinct from local operation"
192	required_stages = []
193	# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
194	[[requirements]]
195	id = "REQ-INST-9"
196	title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
197	required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
198	[[requirements]]
199	id = "REQ-INST-10"
200	title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
201	required_stages = ["impl", "unit"]  # activated M4-D3c: spt_proto::addr::Address::parse (grammar, reserved :/@ → unambiguous split, each component charset-validated) + spt_net::net::registry::resolve/resolve_across (policy: @node exact → local wins → sole live → REFUSE on multiple live nodes [AcrossNodes, force @node] / multiple subnets [AcrossSubnets, force subnet:]; Offline never routed). Per-node epochs are NOT cross-node comparable, so no silent most-recently-active guess. unit = addr 4-shape parse/roundtrip/doubled-delim reject + resolve local/sole/refuse/@node-pin/cross-subnet. Cross-node replication feeding the registry = D4; two-host = D9.
202	[[requirements]]
203	id = "REQ-INST-11"
204	title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
205	required_stages = ["impl", "unit"]  # activated M4-D3f: three local arms. (1) registry rows: spt_net::net::registry::rename_endpoint — collision-checked against EVERY subnet the endpoint is advertised into FIRST (any row, any status holds the name), then moved all-or-nothing; rows verbatim (relabel, not liveness event); new id charset-validated (:/@ reserved). (2) perch dirs: spt_store::rename::rename_perch_in — Self dir + nested <id>-psyche/<id>-w{N} children + every info.json id; refuses while any perch in the tree is live; local SQLite registry rows deliberately untouched (live refuses ⇒ old rows stale ⇒ 4.3 stale-clean drops). (3) CLI `spt rename <old> <new>` wires the perch arm. Seams: daemon-held registry wiring = D4; a-<id> context branch = D6 context store; cross-node ripple = authored (node,epoch) op reconciled newest-wins per hazard 6.5 marker at D4/D6. unit = cross-subnet ripple-verbatim + all-or-nothing collision/NotFound/InvalidId/self-noop + perch tree ripple + refusals-untouched + live-child-blocks + CLI parse.
206	[[requirements]]
207	id = "REQ-INST-12"
208	title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
209	required_stages = ["impl", "unit"]  # activated M4-D3e: spt_store::visibility::VisibilityStore (identity/visibility.json, atomic, degrade-safe) — hidden(E,S,flag) = per-(E,S) override wins both directions, else S.hide_new_endpoints OR E.default_hide (both ship OFF). SubnetRecord.hide_new_endpoints serde-defaulted (legacy subnet.json loads OFF). Enforcement in spt_net::net::registry: excluded = not advertised (advertise_if_visible gates BEFORE the collision check — hidden neither claims nor clashes) AND not routable (resolve_visible/resolve_across_visible refuse even @node pins; exclusion prunes the cross-subnet ambiguity set). unit = OR-of-defaults + override-both-directions + persist/degrade + legacy-load + not-routable + ambiguity-prune + advertise-gate.
210	[[requirements]]
211	id = "REQ-INST-13"
212	title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
213	required_stages = ["impl", "unit"]  # activated M4-D3e: VisibilityStore.sync_subnets per-endpoint membership list + synced(E,S,flag) — the D6 replication gate: subnet must be on the list (unconfigured list syncs NOWHERE, fail-closed; creation flow seeds [home] per ADR-0010) AND visibility gates sync (hidden ⟹ not synced even when listed, ADR-0006 §6). Actual mind replication consuming this gate = D6. unit = listed-visible-syncs + unlisted/unconfigured-no + hidden-gates-sync (defaults + override paths).
214	[[requirements]]
215	id = "REQ-INST-14"
216	title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
217	required_stages = []
218	[[requirements]]
219	id = "REQ-INST-15"
220	title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
221	required_stages = []
222	[[requirements]]
223	id = "REQ-REACH-1"
224	title = "Off-node remote-drive detection + file transfer"
225	required_stages = []
226	[[requirements]]
227	id = "REQ-REACH-2"
228	title = "Remote command execution (deferred, consent-gated)"
229	required_stages = []
230
231	# ───────────────────────────── Local messaging (M1) ─────────────────────────
232	# No PRD R-* covers local message delivery (foundational/implicit). Registered
233	# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
234	# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
235	[[requirements]]
236	id = "REQ-MSG-1"
237	title = "Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)"
238	required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
239	[[requirements]]
240	id = "REQ-MSG-2"
241	title = "spt binary CLI surface: send/ring/ready/poll/list/stop/whoami, stable arg shapes + exit codes"
242	required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired
243	[[requirements]]
244	id = "REQ-MSG-3"
245	title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
246	required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E
247
248	# ───────────────────────────── Networking / pairing ─────────────────────────
249	[[requirements]]
250	id = "REQ-NODE-IDENTITY"
251	title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
252	required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
253	[[requirements]]
254	id = "REQ-NET-1"
255	title = "WAN messaging first-class, behind default-on net feature flag"
256	required_stages = ["impl", "unit"]  # activated M4-D1: spt-net NetEndpoint binds an iroh endpoint to the node's own spt-proto Ed25519 identity (EndpointId == node pubkey) behind the default-on `net` feature, with connect/accept over the SPT_NET_ALPN; unit proves identity-binding + a hermetic loopback QUIC bidi echo. The cross-node WAN `int` stage is the two-host E2E at D9 (precedent: REQ-MSG-1 int added at its E2E task)
257	[[requirements]]
258	id = "REQ-NET-2"
259	title = "n0 relay default + self-host knob + plain-language disclosure"
260	required_stages = ["impl"]  # activated M4-D1: RelayPolicy {N0Default | SelfHosted(RelayMap) | Disabled} maps onto iroh RelayMode (n0 public relays are the zero-config default; self-host is the escape hatch; disabled = LAN/air-gapped + hermetic tests), plus the RELAY_DISCLOSURE plain-language string. Config plumbing — no unit/int stage required
261	[[requirements]]
262	id = "REQ-NET-3"
263	title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
264	required_stages = []
265	[[requirements]]
266	id = "REQ-PAIR-1"
267	title = "TOTP-seeded SPAKE2 pairing"
268	required_stages = ["impl", "unit"]  # activated M4-D2-wire: pairing ALPN ceremony driver (spt-net::net::pairing::wire) runs the 4-msg SPAKE2 ceremony over SPT_PAIR_ALPN, prepended by a responder Announce{epoch} RTT (joiner lacks the responder-authoritative seed epoch that Initiator::start binds into msg_a). Responder gates via PairingRateLimiter + ±1 step + SubnetStore seed; both sides write TrustStore.record on confirm. unit = hermetic loopback E2E (RelayPolicy::Disabled) like endpoint.rs; int = two-host WAN at D9.
269	[[requirements]]
270	id = "REQ-PAIR-2"
271	title = "Local trust store with TOFU + warn-on-change"
272	required_stages = ["impl", "unit"]  # activated M4-D2e: spt-store::trust::TrustStore (identity/trust/peers.json) pins peer node pubkeys per-subnet post-ceremony (TOFU). record() => Pinned/AlreadyTrusted/KeyChanged{existing}; KeyChanged never auto-applied (repin on accepted rotation); is_trusted = connect gate; revoke = trust-delete (pair w/ seed rotation #10). Warn-on-change anchored on optional (subnet,user,label) — dormant until labels exist (GUI/D9), honest v1 residual. int = on-wire trusted-vs-untrusted gate at D2-wire/D9.
273	[[requirements]]
274	id = "REQ-PAIR-3"
275	title = "Fetch current pairing code from any paired node"
276	required_stages = ["impl", "unit"]  # activated M4-D2g: `spt pair show-totp [--subnet <name>|--create-new <name>]` surfaces a subnet's current code + otpauth:// URI off this node (cli.rs decide_show_totp). Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
277	[[requirements]]
278	id = "REQ-PAIR-4"
279	title = "Subnet naming on first pairing"
280	required_stages = ["impl", "unit"]  # activated M4-D2f: SubnetStore::create_subnet names the subnet at creation (sole seed-holder, epoch 1; "naming moves to link-start" ADR-0006); the name rides every ceremony (joiner types it, transcript binds it) and reaches the joiner via D2f seed transfer. unit = wire create_new_then_join_learns_named_subnet. CLI naming prompt is later (daemon wiring, D4+); int = two-host at D9.
281	# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
282	[[requirements]]
283	id = "REQ-PAIR-5"
284	title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
285	required_stages = ["impl", "unit"]  # activated M4-D2f: discovery takes code+subnet-name (the wire Hello carries the name; both are ceremony inputs); rendezvous-token H(name‖TOTP-step) derived in spt-net::net::pairing::rendezvous (TOTP-step = clock-derived window, not seed epoch — D2-wire Q2); seed transfer on join (wire F7 Seed -> SubnetStore::add_joined) makes the joiner a full member/seed-holder. unit = rendezvous tests + wire joiner_becomes_seed_holder/repair_member_keeps_seed. int = relay rendezvous routing + two-host at D9 (Q1 deferral).
286	[[requirements]]
287	id = "REQ-PAIR-6"
288	title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
289	required_stages = ["impl", "unit"]  # activated M4-D2g: elevation.rs probes OS privilege (Linux libc::geteuid()==0; Windows GetTokenInformation(TokenElevation) via windows-sys; cfg-split) — only the binary pulls OS deps. gate_for: Elevated => Show, NotElevated|Unknown => Fallback (fail safe, never leak the code unconfirmed). Refused fetch leaks no code, persists no mint, distinct exit 3, and the fallback names the authenticator app (seed provisioned there at pairing). unit = gate decision both ways + Unknown fallback + no-leak/no-save on refusal; the live OS probe is environment-dependent (manual verification via todlando / D9). The elevated-*agent*-endpoint path (IPC) is D4/D5, not D2g.
290	[[requirements]]
291	id = "REQ-PAIR-7"
292	title = "Subnet icon (inline image metadata, GUI-only consumer)"
293	required_stages = []
294	# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
295	[[requirements]]
296	id = "REQ-SEC-1"
297	title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
298	required_stages = []
299	# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
300	[[requirements]]
301	id = "REQ-NOTIF-1"
302	title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
303	required_stages = []
304	[[requirements]]
305	id = "REQ-NOTIF-2"
306	title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
307	required_stages = []
308
309	# ───────────────────────────── Self-update ──────────────────────────────────
310	[[requirements]]
311	id = "REQ-UPD-1"
312	title = "Peer-propagated update over P2P"
313	required_stages = []
314	[[requirements]]
315	id = "REQ-UPD-2"
316	title = "All binaries signature-verified before handoff"
317	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
318	[[requirements]]
319	id = "REQ-UPD-3"
320	title = "No endpoint process terminates/suspends during self-update"
321	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)
322	[[requirements]]
323	id = "REQ-UPD-4"
324	title = "Update gated on user confirmation by default; opt-in full-auto"
325	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
326	[[requirements]]
327	id = "REQ-UPD-5"
328	title = "spt-core ripple-updates registered adapters"
329	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
330
331	# ───────────────────────────── Terminal wrapper / frontend ──────────────────
332	[[requirements]]
333	id = "REQ-TERM-1"
334	title = "Process-supervisor terminal wrapper hosting broker PTYs"
335	required_stages = ["impl", "unit"]
336	[[requirements]]
337	id = "REQ-TERM-2"
338	title = "session-surface abstraction; send-keys + send-line injection"
339	required_stages = ["impl", "unit"]
340	[[requirements]]
341	id = "REQ-TERM-3"
342	title = "Byte-stream remote terminal streaming for v1"
343	required_stages = ["impl", "unit"]
344	# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
345	[[requirements]]
346	id = "REQ-TERM-4"
347	title = "Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log"
348	required_stages = ["impl", "unit", "int"]
349	[[requirements]]
350	id = "REQ-FRONT-1"
351	title = "Day-one launcher/manager frontend (list/launch/attach/init)"
352	required_stages = []
353
354	# ───────────────────────────── Install / migration / infra ──────────────────
355	[[requirements]]
356	id = "REQ-INSTALL-1"
357	title = "Two install paths; signed one-line script; OS-service registration"
358	required_stages = []
359	[[requirements]]
360	id = "REQ-INSTALL-2"
361	title = "Marketplace-repackaging-friendly install"
362	required_stages = []
363	[[requirements]]
364	id = "REQ-INSTALL-3"
365	title = "Idempotent + interactive-optional first run"
366	required_stages = []
367	[[requirements]]
368	id = "REQ-INSTALL-4"
369	title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"
370	required_stages = []
371	[[requirements]]
372	id = "REQ-MIGRATE-1"
373	title = "Auto-detect and migrate a legacy claude_skill_owl install"
374	required_stages = []
375	[[requirements]]
376	id = "REQ-INFRA-1"
377	title = "GitHub issue tracking for v1; tangled.org as migration target"
378	required_stages = []
379
380	# ───────────────────────────── Documentation ────────────────────────────────
381	[[requirements]]
382	id = "REQ-DOCS-1"
383	title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
384	required_stages = []
385	[[requirements]]
386	id = "REQ-DOCS-2"
387	title = "Sub-10-minute runnable killer quickstart per audience"
388	required_stages = []
389	[[requirements]]
390	id = "REQ-DOCS-3"
391	title = "Diátaxis structure; one canonical way to do X"
392	required_stages = []
393	[[requirements]]
394	id = "REQ-DOCS-4"
395	title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
396	required_stages = []
397	[[requirements]]
398	id = "REQ-DOCS-5"
399	title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
400	required_stages = []
401
402	# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
403	# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
404	# `unit` (and `int` where cross-process/cross-node) — the conformance checklist
405	# becomes a CI gate (PRD success criterion #6).
406
407	[[requirements]]
408	id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
409	title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
410	required_stages = ["impl", "unit"]
411	[[requirements]]
412	id = "REQ-HAZARD-INFO-JSON-TORN-READ"
413	title = "State-file reads tolerate concurrent writes (1.2)"
414	required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
415	[[requirements]]
416	id = "REQ-HAZARD-STALE-INDEX-LOCK"
417	title = "Sweep stale lockfiles on daemon boot (1.3)"
418	required_stages = []
419	[[requirements]]
420	id = "REQ-HAZARD-DEFERRED-DRAIN"
421	title = "Deferred spool rows excluded from the event-stream drain (1.4)"
422	required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
423	[[requirements]]
424	id = "REQ-HAZARD-WORKER-PATH"
425	title = "Single source of truth for Worker/Psyche perch location (1.5)"
426	required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
427	[[requirements]]
428	id = "REQ-HAZARD-PARENT-PID-PREFER"
429	title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
430	required_stages = []
431	[[requirements]]
432	id = "REQ-HAZARD-STDIN-SESSION-ID"
433	title = "Stdin session_id precedence over env (2.2)"
434	required_stages = []
435	[[requirements]]
436	id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
437	title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
438	required_stages = ["impl", "unit"]
439	[[requirements]]
440	id = "REQ-HAZARD-GEN-START-NOW"
441	title = "gen_start = now() on cold-start and handoff (2.4)"
442	required_stages = ["impl", "int"]
443	[[requirements]]
444	id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
445	title = "Ephemeral perch cleanup on every ring exit path (3.1)"
446	required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
447	[[requirements]]
448	id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
449	title = "Stale signoff sentinel does not kill a fresh start (3.2)"
450	required_stages = ["impl", "unit"]
451	[[requirements]]
452	id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
453	title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
454	required_stages = ["impl", "unit"]
455	[[requirements]]
456	id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
457	title = "Envelope decode order, ampersand decoded last (4.1)"
458	required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec
459	[[requirements]]
460	id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
461	title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
462	required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
463	[[requirements]]
464	id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
465	title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
466	required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3)
467	[[requirements]]
468	id = "REQ-HAZARD-ID-CHARSET"
469	title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
470	required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
471	[[requirements]]
472	id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
473	title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
474	required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)
475	[[requirements]]
476	id = "REQ-HAZARD-REGISTRY-CONCURRENT"
477	title = "Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)"
478	required_stages = ["impl", "unit"]  # activated 2026-06-02: busy_timeout-before-WAL ordering (M1 store)
479	[[requirements]]
480	id = "REQ-HAZARD-REGISTRY-EPOCH-LEASE"
481	title = "Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)"
482	required_stages = ["impl", "unit"]  # activated M4-D3b: spt-store::epoch::EpochSource (persisted strictly-increasing per-node counter, never wall-clock) + spt-net::net::registry::merge_instance version-vector lease keyed on (endpoint,node): strictly-greater-epoch wins, equal/lower dropped Stale. unit = stale_active_cannot_clobber_newer_offline + equal_epoch_replay_noop + epoch monotonic/persist/corrupt-safe. Cross-node replication of the merge = D4; chaos/two-host = D9.
483	[[requirements]]
484	id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
485	title = "Deferred rows survive poll drain (4.4)"
486	required_stages = ["impl", "unit"]  # activated M0: spt-store spool deferred rows (T9)
487	[[requirements]]
488	id = "REQ-HAZARD-INBOX-NO-DOUBLE"
489	title = "No double-delivery via legacy inbox (4.5)"
490	required_stages = ["impl", "unit"]  # activated M1 T2: delivery is TCP xor spool, never both
491	[[requirements]]
492	id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
493	title = "Windows PID-recycling false positives guarded (5.1)"
494	required_stages = ["impl", "unit"]  # activated M1 T2: connect-must-succeed guard + dead-pid row clean on delivery
495	[[requirements]]
496	id = "REQ-HAZARD-EBUSY-RENAME"
497	title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
498	required_stages = ["impl", "unit"]  # activated M0: spt-store atomic write (T8)
499	[[requirements]]
500	id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
501	title = "Every harness/git subprocess has a timeout (5.3)"
502	required_stages = ["impl", "unit"]
503	[[requirements]]
504	id = "REQ-HAZARD-UNC-PATH-STRIP"
505	title = "Strip Windows UNC prefix on serialized paths (5.4)"
506	required_stages = ["impl", "unit"]  # activated M0: spt-store to_forward_slash (T8)
507	[[requirements]]
508	id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
509	title = "Single path/registry source of truth; no layout ambiguity (6.1)"
510	required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
511	[[requirements]]
512	id = "REQ-HAZARD-SOFT-CLEANUP"
513	title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
514	required_stages = ["impl", "unit"]  # activated M1 T3: soft_cleanup removes ready marker only, preserves spool + info.json
515	[[requirements]]
516	id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
517	title = "No hard-delete of a parent hosting non-empty children (6.3)"
518	required_stages = ["impl", "unit"]
519	[[requirements]]
520	id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
521	title = "Drop files are daemon-owned single-writer (6.4)"
522	required_stages = ["impl", "unit"]
523	[[requirements]]
524	id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
525	title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
526	required_stages = ["impl", "unit"]
527	# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
528	[[requirements]]
529	id = "REQ-HAZARD-CONPTY-DSR"
530	title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
531	required_stages = ["impl", "unit"]
532	[[requirements]]
533	id = "REQ-HAZARD-LOCAL-API-AUTH"
534	title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
535	required_stages = ["impl", "unit"]
536	[[requirements]]
537	id = "REQ-HAZARD-RESTART-IDEMPOTENT"
538	title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
539	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)
540	[[requirements]]
541	id = "REQ-HAZARD-UPDATE-ROLLBACK"
542	title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
543	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)
544	[[requirements]]
545	id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
546	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)"
547	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
548	# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
549	[[requirements]]
550	id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
551	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)"
552	required_stages = []  # rule 5: activate when delivered. The <EVENT type=reply|notify> parse-and-relay path does not exist yet; M4-D7.5 (psyche outbound relay, feeds D8) builds + activates it
553	[[requirements]]
554	id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
555	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)"
556	required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops
557	# --- 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 ---
558	[[requirements]]
559	id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
560	title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
561	required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
562	[[requirements]]
563	id = "REQ-HAZARD-PAIR-SEED-ROTATION"
564	title = "Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)"
565	required_stages = ["impl", "unit"]  # activated M4-D2c: spt-store::subnet SubnetStore holds N per-subnet TOTP seeds + monotonic epoch; rotate_seed mints fresh seed material + bumps the epoch (the node-removal response), persisted atomically + durable across reload. The epoch is bound into the pairing transcript (REQ-HAZARD-PAIR-TRANSCRIPT-BIND), and the spake stale_epoch/wrong_code tests prove an old seed/epoch can't rejoin. int = on-wire evict-then-rejoin E2E at D9
566	[[requirements]]
567	id = "REQ-HAZARD-PAIR-RATE-LIMIT"
568	title = "Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)"
569	required_stages = ["impl", "unit"]  # activated M4-D2d: spt-net::pairing::ratelimit PairingRateLimiter — one in-flight ceremony per subnet (Busy), shared consecutive-failure counter, exponential backoff (1s→cap 3600s, abandoned ceremonies reclaimed+charged), per-subnet isolated. The ±1 TOTP window is JUSTIFIED (not dropped): serialization + exp backoff caps the guess rate so 3/1e6 success-per-guess stays out of reach inside a 30s code. int = on-wire distributed-guessing rejection at D2-wire/D9.
570

# 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]
roots = ["src", "tests", "docs", "crates"]

[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 = []

# ───────────────────────────── 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"]
[[requirements]]
id = "REQ-SEAM-SPAWN"
title = "spawn-session seam"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-POSTSPAWN"
title = "post-spawn / api bind seam with boot nonce"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-PSYCHE"
title = "spawn-psyche seam (fresh + resume templates)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-SEAM-HISTORY"
title = "History subsystem (fetcher / locate-normalize / native store)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-SEAM-ACTIVITY"
title = "Activity/idle reported via api sentinels, not PTY quiescence"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-INJECT"
title = "inject-input methods configurable per activity-state"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-RESUME"
title = "resume-session seam (fresh-with-preload / continue-existing)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-CAPABILITY"
title = "Hostable endpoint-types capability declaration"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-UPDATE"
title = "Adapter-update avenue (file-pull / delegated command)"
required_stages = ["impl", "unit"]  # activated M3c-C3: manifest [update] gains signing_key (file_pull content key) + self_verifies (delegated attest); adapter_update::plan_adapter_update dispatches each avenue (file_pull verified against the adapter key, delegated delegated-or-skipped)

# ───────────────────────────── Harness contract — api surface ───────────────
[[requirements]]
id = "REQ-API-1"
title = "api prefix and adapter_name on every machinery invocation"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-API-2"
title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-API-3"
title = "commune/signoff are file-drops, not commands"
required_stages = ["impl", "unit", "int"]

# ───────────────────────────── Startup flows ────────────────────────────────
[[requirements]]
id = "REQ-START-1"
title = "Adapters never resolve SPT_HOME; binary on PATH; api bridging only"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-START-2"
title = "Harness-hosted startup: api seed then listen"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-START-3"
title = "spt-hosted startup: spawn-session then api bind (no file)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-START-4"
title = "Adapter-injected env aliases (SPT/OWL/LIVE)"
required_stages = ["impl", "unit"]

# ───────────────────────────── Endpoints / Shells ───────────────────────────
[[requirements]]
id = "REQ-EP-1"
title = "Day-one endpoint types; open type system"
required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
[[requirements]]
id = "REQ-EP-2"
title = "Agent endpoints vs Shells distinction in the type model"
required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
[[requirements]]
id = "REQ-EP-3"
title = "Messaging payloads carry typed operation commands + file blobs"
required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
[[requirements]]
id = "REQ-EP-4"
title = "PresenceChannel broker endpoint (seam day-one)"
required_stages = []
[[requirements]]
id = "REQ-EP-5"
title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
required_stages = []

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = []
[[requirements]]
id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = []
[[requirements]]
id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = []
[[requirements]]
id = "REQ-INST-5"
title = "Two-tier context sync (live to all, project to same-project)"
required_stages = []
[[requirements]]
id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = []
[[requirements]]
id = "REQ-INST-7"
title = "Subnet registry + bare-id resolution policy"
required_stages = ["impl", "unit"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10); cross-node replication = D4; int two-host = D9. unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes.
[[requirements]]
id = "REQ-INST-8"
title = "Remote-control mode distinct from local operation"
required_stages = []
# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-INST-9"
title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
[[requirements]]
id = "REQ-INST-10"
title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
required_stages = ["impl", "unit"]  # activated M4-D3c: spt_proto::addr::Address::parse (grammar, reserved :/@ → unambiguous split, each component charset-validated) + spt_net::net::registry::resolve/resolve_across (policy: @node exact → local wins → sole live → REFUSE on multiple live nodes [AcrossNodes, force @node] / multiple subnets [AcrossSubnets, force subnet:]; Offline never routed). Per-node epochs are NOT cross-node comparable, so no silent most-recently-active guess. unit = addr 4-shape parse/roundtrip/doubled-delim reject + resolve local/sole/refuse/@node-pin/cross-subnet. Cross-node replication feeding the registry = D4; two-host = D9.
[[requirements]]
id = "REQ-INST-11"
title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
required_stages = ["impl", "unit"]  # activated M4-D3f: three local arms. (1) registry rows: spt_net::net::registry::rename_endpoint — collision-checked against EVERY subnet the endpoint is advertised into FIRST (any row, any status holds the name), then moved all-or-nothing; rows verbatim (relabel, not liveness event); new id charset-validated (:/@ reserved). (2) perch dirs: spt_store::rename::rename_perch_in — Self dir + nested <id>-psyche/<id>-w{N} children + every info.json id; refuses while any perch in the tree is live; local SQLite registry rows deliberately untouched (live refuses ⇒ old rows stale ⇒ 4.3 stale-clean drops). (3) CLI `spt rename <old> <new>` wires the perch arm. Seams: daemon-held registry wiring = D4; a-<id> context branch = D6 context store; cross-node ripple = authored (node,epoch) op reconciled newest-wins per hazard 6.5 marker at D4/D6. unit = cross-subnet ripple-verbatim + all-or-nothing collision/NotFound/InvalidId/self-noop + perch tree ripple + refusals-untouched + live-child-blocks + CLI parse.
[[requirements]]
id = "REQ-INST-12"
title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
required_stages = ["impl", "unit"]  # activated M4-D3e: spt_store::visibility::VisibilityStore (identity/visibility.json, atomic, degrade-safe) — hidden(E,S,flag) = per-(E,S) override wins both directions, else S.hide_new_endpoints OR E.default_hide (both ship OFF). SubnetRecord.hide_new_endpoints serde-defaulted (legacy subnet.json loads OFF). Enforcement in spt_net::net::registry: excluded = not advertised (advertise_if_visible gates BEFORE the collision check — hidden neither claims nor clashes) AND not routable (resolve_visible/resolve_across_visible refuse even @node pins; exclusion prunes the cross-subnet ambiguity set). unit = OR-of-defaults + override-both-directions + persist/degrade + legacy-load + not-routable + ambiguity-prune + advertise-gate.
[[requirements]]
id = "REQ-INST-13"
title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
required_stages = ["impl", "unit"]  # activated M4-D3e: VisibilityStore.sync_subnets per-endpoint membership list + synced(E,S,flag) — the D6 replication gate: subnet must be on the list (unconfigured list syncs NOWHERE, fail-closed; creation flow seeds [home] per ADR-0010) AND visibility gates sync (hidden ⟹ not synced even when listed, ADR-0006 §6). Actual mind replication consuming this gate = D6. unit = listed-visible-syncs + unlisted/unconfigured-no + hidden-gates-sync (defaults + override paths).
[[requirements]]
id = "REQ-INST-14"
title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
required_stages = []
[[requirements]]
id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = []
[[requirements]]
id = "REQ-REACH-1"
title = "Off-node remote-drive detection + file transfer"
required_stages = []
[[requirements]]
id = "REQ-REACH-2"
title = "Remote command execution (deferred, consent-gated)"
required_stages = []

# ───────────────────────────── Local messaging (M1) ─────────────────────────
# No PRD R-* covers local message delivery (foundational/implicit). Registered
# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
[[requirements]]
id = "REQ-MSG-1"
title = "Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)"
required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
[[requirements]]
id = "REQ-MSG-2"
title = "spt binary CLI surface: send/ring/ready/poll/list/stop/whoami, stable arg shapes + exit codes"
required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired
[[requirements]]
id = "REQ-MSG-3"
title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E

# ───────────────────────────── Networking / pairing ─────────────────────────
[[requirements]]
id = "REQ-NODE-IDENTITY"
title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
[[requirements]]
id = "REQ-NET-1"
title = "WAN messaging first-class, behind default-on net feature flag"
required_stages = ["impl", "unit"]  # activated M4-D1: spt-net NetEndpoint binds an iroh endpoint to the node's own spt-proto Ed25519 identity (EndpointId == node pubkey) behind the default-on `net` feature, with connect/accept over the SPT_NET_ALPN; unit proves identity-binding + a hermetic loopback QUIC bidi echo. The cross-node WAN `int` stage is the two-host E2E at D9 (precedent: REQ-MSG-1 int added at its E2E task)
[[requirements]]
id = "REQ-NET-2"
title = "n0 relay default + self-host knob + plain-language disclosure"
required_stages = ["impl"]  # activated M4-D1: RelayPolicy {N0Default | SelfHosted(RelayMap) | Disabled} maps onto iroh RelayMode (n0 public relays are the zero-config default; self-host is the escape hatch; disabled = LAN/air-gapped + hermetic tests), plus the RELAY_DISCLOSURE plain-language string. Config plumbing — no unit/int stage required
[[requirements]]
id = "REQ-NET-3"
title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
required_stages = []
[[requirements]]
id = "REQ-PAIR-1"
title = "TOTP-seeded SPAKE2 pairing"
required_stages = ["impl", "unit"]  # activated M4-D2-wire: pairing ALPN ceremony driver (spt-net::net::pairing::wire) runs the 4-msg SPAKE2 ceremony over SPT_PAIR_ALPN, prepended by a responder Announce{epoch} RTT (joiner lacks the responder-authoritative seed epoch that Initiator::start binds into msg_a). Responder gates via PairingRateLimiter + ±1 step + SubnetStore seed; both sides write TrustStore.record on confirm. unit = hermetic loopback E2E (RelayPolicy::Disabled) like endpoint.rs; int = two-host WAN at D9.
[[requirements]]
id = "REQ-PAIR-2"
title = "Local trust store with TOFU + warn-on-change"
required_stages = ["impl", "unit"]  # activated M4-D2e: spt-store::trust::TrustStore (identity/trust/peers.json) pins peer node pubkeys per-subnet post-ceremony (TOFU). record() => Pinned/AlreadyTrusted/KeyChanged{existing}; KeyChanged never auto-applied (repin on accepted rotation); is_trusted = connect gate; revoke = trust-delete (pair w/ seed rotation #10). Warn-on-change anchored on optional (subnet,user,label) — dormant until labels exist (GUI/D9), honest v1 residual. int = on-wire trusted-vs-untrusted gate at D2-wire/D9.
[[requirements]]
id = "REQ-PAIR-3"
title = "Fetch current pairing code from any paired node"
required_stages = ["impl", "unit"]  # activated M4-D2g: `spt pair show-totp [--subnet <name>|--create-new <name>]` surfaces a subnet's current code + otpauth:// URI off this node (cli.rs decide_show_totp). Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
[[requirements]]
id = "REQ-PAIR-4"
title = "Subnet naming on first pairing"
required_stages = ["impl", "unit"]  # activated M4-D2f: SubnetStore::create_subnet names the subnet at creation (sole seed-holder, epoch 1; "naming moves to link-start" ADR-0006); the name rides every ceremony (joiner types it, transcript binds it) and reaches the joiner via D2f seed transfer. unit = wire create_new_then_join_learns_named_subnet. CLI naming prompt is later (daemon wiring, D4+); int = two-host at D9.
# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-PAIR-5"
title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
required_stages = ["impl", "unit"]  # activated M4-D2f: discovery takes code+subnet-name (the wire Hello carries the name; both are ceremony inputs); rendezvous-token H(name‖TOTP-step) derived in spt-net::net::pairing::rendezvous (TOTP-step = clock-derived window, not seed epoch — D2-wire Q2); seed transfer on join (wire F7 Seed -> SubnetStore::add_joined) makes the joiner a full member/seed-holder. unit = rendezvous tests + wire joiner_becomes_seed_holder/repair_member_keeps_seed. int = relay rendezvous routing + two-host at D9 (Q1 deferral).
[[requirements]]
id = "REQ-PAIR-6"
title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
required_stages = ["impl", "unit"]  # activated M4-D2g: elevation.rs probes OS privilege (Linux libc::geteuid()==0; Windows GetTokenInformation(TokenElevation) via windows-sys; cfg-split) — only the binary pulls OS deps. gate_for: Elevated => Show, NotElevated|Unknown => Fallback (fail safe, never leak the code unconfirmed). Refused fetch leaks no code, persists no mint, distinct exit 3, and the fallback names the authenticator app (seed provisioned there at pairing). unit = gate decision both ways + Unknown fallback + no-leak/no-save on refusal; the live OS probe is environment-dependent (manual verification via todlando / D9). The elevated-*agent*-endpoint path (IPC) is D4/D5, not D2g.
[[requirements]]
id = "REQ-PAIR-7"
title = "Subnet icon (inline image metadata, GUI-only consumer)"
required_stages = []
# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
[[requirements]]
id = "REQ-SEC-1"
title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
required_stages = []
# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-NOTIF-1"
title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
required_stages = []
[[requirements]]
id = "REQ-NOTIF-2"
title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
required_stages = []

# ───────────────────────────── Self-update ──────────────────────────────────
[[requirements]]
id = "REQ-UPD-1"
title = "Peer-propagated update over P2P"
required_stages = []
[[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

# ───────────────────────────── 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"]
[[requirements]]
id = "REQ-TERM-3"
title = "Byte-stream remote terminal streaming for v1"
required_stages = ["impl", "unit"]
# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
[[requirements]]
id = "REQ-TERM-4"
title = "Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-FRONT-1"
title = "Day-one launcher/manager frontend (list/launch/attach/init)"
required_stages = []

# ───────────────────────────── Install / migration / infra ──────────────────
[[requirements]]
id = "REQ-INSTALL-1"
title = "Two install paths; signed one-line script; OS-service registration"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-2"
title = "Marketplace-repackaging-friendly install"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-3"
title = "Idempotent + interactive-optional first run"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-4"
title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"
required_stages = []
[[requirements]]
id = "REQ-MIGRATE-1"
title = "Auto-detect and migrate a legacy claude_skill_owl install"
required_stages = []
[[requirements]]
id = "REQ-INFRA-1"
title = "GitHub issue tracking for v1; tangled.org as migration target"
required_stages = []

# ───────────────────────────── Documentation ────────────────────────────────
[[requirements]]
id = "REQ-DOCS-1"
title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
required_stages = []
[[requirements]]
id = "REQ-DOCS-2"
title = "Sub-10-minute runnable killer quickstart per audience"
required_stages = []
[[requirements]]
id = "REQ-DOCS-3"
title = "Diátaxis structure; one canonical way to do X"
required_stages = []
[[requirements]]
id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = []
[[requirements]]
id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = []

# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
# `unit` (and `int` where cross-process/cross-node) — the conformance checklist
# becomes a CI gate (PRD success criterion #6).

[[requirements]]
id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-INFO-JSON-TORN-READ"
title = "State-file reads tolerate concurrent writes (1.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
[[requirements]]
id = "REQ-HAZARD-STALE-INDEX-LOCK"
title = "Sweep stale lockfiles on daemon boot (1.3)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DEFERRED-DRAIN"
title = "Deferred spool rows excluded from the event-stream drain (1.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
[[requirements]]
id = "REQ-HAZARD-WORKER-PATH"
title = "Single source of truth for Worker/Psyche perch location (1.5)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-PARENT-PID-PREFER"
title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-STDIN-SESSION-ID"
title = "Stdin session_id precedence over env (2.2)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-GEN-START-NOW"
title = "gen_start = now() on cold-start and handoff (2.4)"
required_stages = ["impl", "int"]
[[requirements]]
id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
title = "Ephemeral perch cleanup on every ring exit path (3.1)"
required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
[[requirements]]
id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
title = "Stale signoff sentinel does not kill a fresh start (3.2)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
[[requirements]]
id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3)
[[requirements]]
id = "REQ-HAZARD-ID-CHARSET"
title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-CONCURRENT"
title = "Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: busy_timeout-before-WAL ordering (M1 store)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-EPOCH-LEASE"
title = "Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)"
required_stages = ["impl", "unit"]  # activated M4-D3b: spt-store::epoch::EpochSource (persisted strictly-increasing per-node counter, never wall-clock) + spt-net::net::registry::merge_instance version-vector lease keyed on (endpoint,node): strictly-greater-epoch wins, equal/lower dropped Stale. unit = stale_active_cannot_clobber_newer_offline + equal_epoch_replay_noop + epoch monotonic/persist/corrupt-safe. Cross-node replication of the merge = D4; chaos/two-host = D9.
[[requirements]]
id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
title = "Deferred rows survive poll drain (4.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool deferred rows (T9)
[[requirements]]
id = "REQ-HAZARD-INBOX-NO-DOUBLE"
title = "No double-delivery via legacy inbox (4.5)"
required_stages = ["impl", "unit"]  # activated M1 T2: delivery is TCP xor spool, never both
[[requirements]]
id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
title = "Windows PID-recycling false positives guarded (5.1)"
required_stages = ["impl", "unit"]  # activated M1 T2: connect-must-succeed guard + dead-pid row clean on delivery
[[requirements]]
id = "REQ-HAZARD-EBUSY-RENAME"
title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store atomic write (T8)
[[requirements]]
id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
title = "Every harness/git subprocess has a timeout (5.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-UNC-PATH-STRIP"
title = "Strip Windows UNC prefix on serialized paths (5.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store to_forward_slash (T8)
[[requirements]]
id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
title = "Single path/registry source of truth; no layout ambiguity (6.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-SOFT-CLEANUP"
title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
required_stages = ["impl", "unit"]  # activated M1 T3: soft_cleanup removes ready marker only, preserves spool + info.json
[[requirements]]
id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
title = "No hard-delete of a parent hosting non-empty children (6.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
title = "Drop files are daemon-owned single-writer (6.4)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = ["impl", "unit"]
# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]
[[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 = []  # rule 5: activate when delivered. The <EVENT type=reply|notify> parse-and-relay path does not exist yet; M4-D7.5 (psyche outbound relay, feeds D8) builds + activates it
[[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 = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops
# --- 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"
title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-SEED-ROTATION"
title = "Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)"
required_stages = ["impl", "unit"]  # activated M4-D2c: spt-store::subnet SubnetStore holds N per-subnet TOTP seeds + monotonic epoch; rotate_seed mints fresh seed material + bumps the epoch (the node-removal response), persisted atomically + durable across reload. The epoch is bound into the pairing transcript (REQ-HAZARD-PAIR-TRANSCRIPT-BIND), and the spake stale_epoch/wrong_code tests prove an old seed/epoch can't rejoin. int = on-wire evict-then-rejoin E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-RATE-LIMIT"
title = "Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)"
required_stages = ["impl", "unit"]  # activated M4-D2d: spt-net::pairing::ratelimit PairingRateLimiter — one in-flight ceremony per subnet (Busy), shared consecutive-failure counter, exponential backoff (1s→cap 3600s, abandoned ceremonies reclaimed+charged), per-subnet isolated. The ±1 TOTP window is JUSTIFIED (not dropped): serialization + exp backoff caps the guess rate so 3/1e6 success-per-guess stays out of reach inside a 30s code. int = on-wire distributed-guessing rejection at D2-wire/D9.


crates\spt-daemon\tests\broker.rs
crates\spt-daemon\src\msg.rs
crates\spt-daemon\src\broker.rs
crates\spt-daemon\src\brain.rs