<system-reminder>[Truncated: PARTIAL view — showing lines 1-435 of 683 total (33356 tokens, cap 25000). Call Read with offset=436 limit=435 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>

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"]
71	[[requirements]]
72	id = "REQ-SEAM-SPAWN"
73	title = "spawn-session seam"
74	required_stages = ["impl", "unit"]
75	[[requirements]]
76	id = "REQ-SEAM-POSTSPAWN"
77	title = "post-spawn / api bind seam with boot nonce"
78	required_stages = ["impl", "unit"]
79	[[requirements]]
80	id = "REQ-SEAM-PSYCHE"
81	title = "spawn-psyche seam (fresh + resume templates)"
82	required_stages = ["impl", "unit", "int"]
83	[[requirements]]
84	id = "REQ-SEAM-HISTORY"
85	title = "History subsystem (fetcher / locate-normalize / native store)"
86	required_stages = ["impl", "unit", "int"]
87	[[requirements]]
88	id = "REQ-SEAM-ACTIVITY"
89	title = "Activity/idle reported via api sentinels, not PTY quiescence"
90	required_stages = ["impl", "unit"]
91	[[requirements]]
92	id = "REQ-SEAM-INJECT"
93	title = "inject-input methods configurable per activity-state"
94	required_stages = ["impl", "unit"]
95	[[requirements]]
96	id = "REQ-SEAM-RESUME"
97	title = "resume-session seam (fresh-with-preload / continue-existing)"
98	required_stages = ["impl", "unit"]
99	[[requirements]]
100	id = "REQ-SEAM-CAPABILITY"
101	title = "Hostable endpoint-types capability declaration"
102	required_stages = ["impl", "unit"]
103	[[requirements]]
104	id = "REQ-SEAM-UPDATE"
105	title = "Adapter-update avenue (file-pull / delegated command)"
106	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)
107
108	# ───────────────────────────── Harness contract — api surface ───────────────
109	[[requirements]]
110	id = "REQ-API-1"
111	title = "api prefix and adapter_name on every machinery invocation"
112	required_stages = ["impl", "unit", "int"]
113	[[requirements]]
114	id = "REQ-API-2"
115	title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
116	required_stages = ["impl", "unit", "int"]
117	[[requirements]]
118	id = "REQ-API-3"
119	title = "commune/signoff are file-drops, not commands"
120	required_stages = ["impl", "unit", "int"]
121
122	# ───────────────────────────── Startup flows ────────────────────────────────
123	[[requirements]]
124	id = "REQ-START-1"
125	title = "Adapters never resolve SPT_HOME; binary on PATH; api bridging only"
126	required_stages = ["impl", "unit"]
127	[[requirements]]
128	id = "REQ-START-2"
129	title = "Harness-hosted startup: api seed then listen"
130	required_stages = ["impl", "unit", "int"]
131	[[requirements]]
132	id = "REQ-START-3"
133	title = "spt-hosted startup: spawn-session then api bind (no file)"
134	required_stages = ["impl", "unit", "int"]
135	[[requirements]]
136	id = "REQ-START-4"
137	title = "Adapter-injected env aliases (SPT/OWL/LIVE)"
138	required_stages = ["impl", "unit"]
139
140	# ───────────────────────────── Endpoints / Shells ───────────────────────────
141	[[requirements]]
142	id = "REQ-EP-1"
143	title = "Day-one endpoint types; open type system"
144	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
145	[[requirements]]
146	id = "REQ-EP-2"
147	title = "Agent endpoints vs Shells distinction in the type model"
148	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
149	[[requirements]]
150	id = "REQ-EP-3"
151	title = "Messaging payloads carry typed operation commands + file blobs"
152	required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
153	[[requirements]]
154	id = "REQ-EP-4"
155	title = "PresenceChannel broker endpoint (seam day-one)"
156	required_stages = ["impl", "unit"]  # activated M4-D4c: the broker-owned PresenceLog — a seq'd ring of {connected|disconnected, conn_id, remote_id_hex} events mirroring the StreamLog discipline. register_conn appends connected + spawns a closed-watcher (conn.closed() → conn row removed → disconnected appended), so the conn table finally reflects liveness; the brain subscribes via net-presence-subscribe {from_seq} and consumes net-presence-event under the same contiguous/dedup/gap cursor discipline as streams (brain-restart cursor-resume). The full PresenceChannel *endpoint* (agent-visible channel) builds on this broker seam later; int = two-host at D9.
157	[[requirements]]
158	id = "REQ-EP-5"
159	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"
160	required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags
161
162	# ───────────────────────────── Instances ────────────────────────────────────
163	[[requirements]]
164	id = "REQ-INST-1"
165	title = "endpoint ID vs instance split (adapter-agnostic ID)"
166	required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
167	[[requirements]]
168	id = "REQ-INST-2"
169	title = "Per-node files, synced Psyche mind"
170	required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
171	[[requirements]]
172	id = "REQ-INST-3"
173	title = "Dormant (warm) / suspended (cold) resting states"
174	required_stages = ["doc", "impl", "unit"]  # doc activated M4-D9-3: docs/DORMANCY-BUDGET.md — the measured warm/cold policy lock (ADR-0003 #9 closed: warm default confirmed, zero idle CPU, RSS-only cost; auto-suspend opt-in default OFF). impl/unit activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md
175	[[requirements]]
176	id = "REQ-INST-4"
177	title = "active to dormant/suspended fires a transition echo commune"
178	required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant
179	[[requirements]]
180	id = "REQ-INST-5"
181	title = "Two-tier context sync (live to all, project to same-project)"
182	required_stages = ["impl", "unit", "int"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both directions bootstrap-pull a mind the puller never held, registry-derived want-refs, gate served remotely, HFENDULEAM↔gravity). unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
183	[[requirements]]
184	id = "REQ-INST-6"
185	title = "Deferred messages not delivered to dormant/suspended instances"
186	required_stages = ["impl", "unit", "int"]  # activated M5-D5a/b: the gate is state-keyed at the hook-channel consumer sites — resting::deferred_held reads the owner's durable rest record at drain time; "held" = (deferred=1) ∧ (Dormant|Suspended); poll_drain + cmd_worker_poll gated (the KH 1.4 "all sites agree" sweep — shellchan peek ungated: no deferred writer targets shell perches; ring's ephemeral perch recordless by construction = legacy fallback). Release is by construction at the wake edge: the persisted Active state stops the narrowing, the unchanged delivered-mark discipline keeps it exactly-once. D5b adds the remote arm: StreamFamily::Rest + resthost request/serve (remote-drive trust class — access_check on the handshake-proven origin, no grant gate; refusal = finish-no-reply), cmd_rest qualified-form lift via wansend::wan_rest. unit = held-while-dormant+suspended / non-deferred-untouched / released-once-on-wake / recordless-flows + wire-record roundtrip + demux. int = loopback two-broker E2E (suspend cross-node, gate holds, replay NO_EDGE, wake cross-node releases exactly-once + wake effects, whitelist negative). doc = DEFERRED.md remote-fork note. Rig [twohost] leg waits for D9a.
187	[[requirements]]
188	id = "REQ-INST-7"
189	title = "Subnet registry + bare-id resolution policy"
190	required_stages = ["impl", "unit", "int"]  # 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 delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both sides' perch rows replicate into the other's gated registry over the real wire). unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
191	[[requirements]]
192	id = "REQ-INST-8"
193	title = "Remote-control mode distinct from local operation"
194	required_stages = ["impl", "unit", "int"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — A types into B's live PTY across the rig and reads the echo back)
195	# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
196	[[requirements]]
197	id = "REQ-INST-9"
198	title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
199	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.
200	[[requirements]]
201	id = "REQ-INST-10"
202	title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
203	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.
204	[[requirements]]
205	id = "REQ-INST-11"
206	title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
207	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.
208	[[requirements]]
209	id = "REQ-INST-12"
210	title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
211	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.
212	[[requirements]]
213	id = "REQ-INST-13"
214	title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
215	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).
216	[[requirements]]
217	id = "REQ-INST-14"
218	title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
219	required_stages = ["doc", "impl", "unit"]  # activated M4-D9-4: resources: Option<String> on the registry Instance (additive serde-default, rides RegistryUpdate replication + the epoch lease — a blurb edit is an ordinary epoch-bumped update, NO new record kind/store/merge surface) + resource_projection (the (id,node,status,blurb) yellow-pages view over VISIBLE+routable rows — same exclusion closure as resolution, ADR-0006 §6 leak-free by construction) + both-authored sourcing (info.json resources via spt resources set wins; daemon.json resources_blurb node seed fills the gap) + spt resources set/show/list CLI off the stale-tolerant registry snapshots. The whitelist leg of the gate (viewer-node filtering) rides the deferred consent/whitelist generalization — visibility is the v1 gate exactly as CONTEXT scopes it. doc = CONTEXT §resource advertisement; unit = lease-ordered blurb + pre-D9-4 row parses + hidden-never-listed/offline-skipped/absent-renders-clean + both-authored advertisement + runtime refine epoch-bumps
220	[[requirements]]
221	id = "REQ-INST-15"
222	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)"
223	required_stages = ["doc", "impl", "unit"]  # activated M4-D9-5: spt-store::home (assign_home matrix: sole auto / several refuse-and-qualify / unpaired local-only; stamp_creation_fields = the ONE creation seam shared by spt ready + api listen/bind — NEW assigns hard + seeds sync_subnets=[home], REVIVE carries home/adapter/blurb/knobs forward so a re-bind never wipes them; adopt_for_unset = first-join adoption, sole-subnet only, never re-homes; NO setter — immutability by construction) + info.json home_subnet/adapter + ContextStore::fork_endpoint (one-time copy of live+project tiers as parentless seed commits — copied-then-diverged, no shared history) / remove_endpoint (--delete-source, exactly the source) + spt fork CLI (refusal-first gates: same-id, target membership, local + target-registry join-time collision; same-node v1, remote arm = M5 with instantiate-anywhere consent). doc = ADR-0010 delivered note; unit = assignment matrix + stamp news/revives + adoption + fork copies-then-diverges + collision classes + delete-source-exact
224	[[requirements]]
225	id = "REQ-REACH-1"
226	title = "Off-node remote-drive detection + file transfer"
227	required_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — file fetch off B + push back to B across the rig; chunked xfer through the dispatcher gate)
228	[[requirements]]
229	id = "REQ-REACH-2"
230	title = "Remote command execution (deferred, consent-gated)"
231	required_stages = []  # rule 5: deferred by design (PRD: consent-gated remote exec is the highest-risk capability; workarounds exist) — M5+ with the instantiate-anywhere consent model
232
233	# ───────────────────────────── Local messaging (M1) ─────────────────────────
234	# No PRD R-* covers local message delivery (foundational/implicit). Registered
235	# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
236	# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
237	[[requirements]]
238	id = "REQ-MSG-1"
239	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__)"
240	required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
241	[[requirements]]
242	id = "REQ-MSG-2"
243	title = "spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes"
244	required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired. M7 amendment (plan decision 2, 2026-06-05): `poll` removed — it duplicated `ready` (same run_listen dispatch); `ready --once` absorbs the drain-then-exit semantics. `spt api poll`/`api worker-poll` (machinery namespace) are unaffected. Pre-1.0, zero users, no deprecation shim.
245	[[requirements]]
246	id = "REQ-MSG-3"
247	title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
248	required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E
249	[[requirements]]
250	id = "REQ-MSG-4"
251	title = "Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type=\"msg\" from=…> otherwise, chunk oversized lines into EVENT-PART"
252	required_stages = ["impl", "unit", "int"]  # activated 2026-06-06 (envelope-missing fix): spt-msg::emit render seam + both listener loops (spt ready run_listen, spt api listen cmd_listen); int = quickstart E2E asserts the envelope shape on the live + backlog paths. Hook-channel drains (api poll / worker-poll) stay raw-frame by contract.
253
254	# ───────────────────────────── Networking / pairing ─────────────────────────
255	[[requirements]]
256	id = "REQ-NODE-IDENTITY"
257	title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
258	required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
259	[[requirements]]
260	id = "REQ-NET-1"
261	title = "WAN messaging first-class, behind default-on net feature flag"
262	required_stages = ["impl", "unit", "int"]  # 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. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — a real WAN message lands in the remote perch spool, HFENDULEAM↔gravity over tailscale, [twohost] CI run)
263	[[requirements]]
264	id = "REQ-NET-2"
265	title = "n0 relay default + self-host knob + plain-language disclosure"
266	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
267	[[requirements]]
268	id = "REQ-NET-3"
269	title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
270	required_stages = ["impl", "unit"]  # activated M4-D6c: git-native bundle sync (ADR-0013). D6c-1 store core: BranchStore bundle plumbing (create_bundle ^have incremental w/ unknown-have full fallback + up-to-date skip; fetch_bundle quarantines tips under refs/spt-sync/ — incoming can never clobber refs/heads before the driver rules; merge_commit_in_worktree two-parent join, FF for all-accepted propagation) + spt_store::syncmerge::apply_fetched — per-file vector verdicts (dominate→accept verbatim/dominated→drop/concurrent→record_conflict, hazard 6.6), replicated .conflicts artifacts with resurrection guard (local-dominates-or-equal ⟹ stale echo dropped), dominating accept clears pending artifacts (the only legal clear), drop-only passes still join the DAG (re-pull short-circuits on ancestry). D6c-2 = wire serve/request over broker QUIC + synced/visibility server gate. int = D9 two-host E2E. unit = adopt/incremental-FF/idempotent-repull + dominated-drop-joins-DAG + concurrent-surfaces-both-nodes + reconciled-write-propagates-clears + p-tier-union + legacy-⊥⊥-surfaces
271	[[requirements]]
272	id = "REQ-PAIR-1"
273	title = "TOTP-seeded SPAKE2 pairing"
274	required_stages = ["impl", "unit", "int"]  # 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 activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — the real ceremony over SPT_PAIR_ALPN cross-host; trust pinned both sides).
275	[[requirements]]
276	id = "REQ-PAIR-2"
277	title = "Local trust store with TOFU + warn-on-change"
278	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.
279	[[requirements]]
280	id = "REQ-PAIR-3"
281	title = "Fetch current pairing code from any paired node"
282	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). M7 NOTE: surface moves to `spt subnet show-code [name]` (renamed from show-totp in the M7 UX grill) and creation splits into `spt subnet create <name>` (REQ-SUBNET-1); the gate/resolution semantics here are unchanged. 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.
283	[[requirements]]
284	id = "REQ-PAIR-4"
285	title = "Subnet naming on first pairing"
286	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+) — delivered by M7 REQ-SUBNET-2 (guided `spt subnet join`); int = two-host at D9.
287	# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
288	[[requirements]]
289	id = "REQ-PAIR-5"
290	title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
291	required_stages = ["impl", "unit", "int"]  # 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 activated M4-D9-6: two-host rig seed transfer green (tests/twohost.rs — the joiner holds the subnet seed post-ceremony); relay rendezvous routing stays the M5 Q1 deferral.
292	[[requirements]]
293	id = "REQ-PAIR-6"
294	title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
295	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.
296	[[requirements]]
297	id = "REQ-PAIR-7"
298	title = "Subnet icon (inline image metadata, GUI-only consumer)"
299	required_stages = []  # rule 5: GUI milestone (user 2026-06-04) — inline image metadata has no consumer until a GUI exists
300	# ── M7 subnet & quickstart UX (M7-PLAN.md, grilled 2026-06-05; activate at M7 start, rule 5) ──
301	[[requirements]]
302	id = "REQ-SUBNET-1"
303	title = "spt subnet noun namespace: status view (bare + status [NAME] [--nodes]), create (QR/otpauth), show-code; spt pair deleted"
304	required_stages = ["impl", "unit"]  # activated M7 D1 (2026-06-05): namespace + create/show-totp ports land; D2 adds the full status views. M7 D1/D2 (plan decisions 3, 5, 6, 11). Bare `spt subnet` = flagless status (names, paired-node counts, endpoint counts; never epochs/codes; hint footer; zero-subnet explanatory text). `status [NAME] [--nodes]` adds per-node rows (label, online/offline via gossip-recency + probe-stale hybrid, online/total endpoints). `create <NAME>` mints (sole seed-holder) + prints code/otpauth/terminal-QR + join hint. `show-code [name]` (nee show-totp) keeps REQ-PAIR-3/6 semantics + now re-provisions the full QR/expiry material. `spt pair` namespace removed outright (no shim).
305	[[requirements]]
306	id = "REQ-SUBNET-2"
307	title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
308	required_stages = ["impl", "unit", "int"]  # activated M7 D3 (2026-06-05); int added M7 D5: the twohost ladder's pairing rung IS the product surface now — brain pair-join IPC (role A) against the daemon-hosted always-on responder (role B), replacing the test-harness-driven ceremony as the pairing evidence. M7 D3 (plan decisions 8, 10) — the join ceremony's PRODUCT surface (the SPAKE2 wire is rig-proven but test-only today). Every member daemon hosts the pre-trust pairing ALPN responder ALWAYS-ON (ADR-0005 one-sided-UX intent; the red-team #11 subnet-global rate limiter + one-ceremony-per-subnet is the standing-listener guard). `spt subnet join [NAME] [--code <CODE>]`: prompts for missing pieces, LAN+relay rendezvous, clean wrong-code re-prompt (bounded, limiter surfaced honestly), actionable no-seed-holder hints, seed transfer + trust pinning + REQ-INST-15 home adoption on join. int = twohost ladder gains a product-surface join rung (CLI initiator vs daemon responder) replacing the test-harness-driven ceremony as pairing evidence.
309	[[requirements]]
310	id = "REQ-SUBNET-3"
311	title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
312	required_stages = ["impl", "unit"]  # activated M7 D2 (2026-06-05): label field + hostname stamp + addressable qualifiers land. M7 D2 (plan decision 7). Label defaults to OS hostname, re-checked at daemon startup (hostname change updates it), rides existing registry gossip as an additive serde-default field (old rows parse clean). Pubkey stays the identity; @node qualifiers accept label OR key-prefix; non-unique label resolution refuses-and-qualifies listing candidate key prefixes (CONTEXT.md §node label). Renders `HFENDULEAM (bcead52b…)` in subnet views.
313	[[requirements]]
314	id = "REQ-SUBNET-4"
315	title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
316	required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): create + join gates live (REQ-PAIR-6 machinery, exit 3, gate-first ordering); ADR-0005 amendment recorded. M7 D3 (plan decision 9). `subnet create` and `subnet join` require OS elevation via the existing REQ-PAIR-6 gate machinery (exit 3 on refusal, no leak/no mutation): an unprivileged process must not mint+leak a subnet secret nor enroll the machine into an attacker's subnet. `subnet status` stays ungated (read-only, no secrets). CONTEXT.md §Pairing & trust updated 2026-06-05.
317	[[requirements]]
318	id = "REQ-DOCS-6"
319	title = "spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)"
320	required_stages = ["impl", "unit", "int"]  # activated M7 D1 (2026-06-05); int added M7 D5: quickstart_e2e asserts the how-to texts name the commands the published guide's prompt blocks rely on. M7 D1/D4 (plan decision 12). Visible command, Agent-commands help group. Topics v1: `ready` (background-task guidance, --once fallback loop, reply mechanics) + `send` (send/ring, reply-to, SENT vs QUEUED); bare `how-to` lists topics. Single source = the binary — docs-site never duplicates the content (the DOCS-STRATEGY anti-drift posture applied to agent guidance); quickstart prompt blocks instruct agents to run it and follow. int = quickstart_e2e asserts the how-to output names the commands the guide relies on.
321
322	# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
323	[[requirements]]
324	id = "REQ-SEC-1"
325	title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
326	required_stages = ["impl", "unit"]
327	# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
328	[[requirements]]
329	id = "REQ-NOTIF-1"
330	title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
331	required_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — insert on A fires on B; dismiss on B replicates back to A; the update consent notif also surfaced through the same primitive)
332	[[requirements]]
333	id = "REQ-NOTIF-2"
334	title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
335	required_stages = ["doc", "impl", "unit", "int"]  # int activated M5-D9b: the rig toast rung (tests/twohost.rs — A's notify rendered through B's [session.notif] shell template, run 26998058816 real_mode=true)
336
337	# ───────────────────────────── Self-update ──────────────────────────────────
338	[[requirements]]
339	id = "REQ-UPD-1"
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"]  # activated 2026-06-06: first slice = signed update-set metadata + verification, platform artifact selection, cache/propagation/apply compatibility, maintainer xtask helpers
362
363	# ───────────────────────────── Terminal wrapper / frontend ──────────────────
364	[[requirements]]
365	id = "REQ-TERM-1"
366	title = "Process-supervisor terminal wrapper hosting broker PTYs"
367	required_stages = ["impl", "unit"]
368	[[requirements]]
369	id = "REQ-TERM-2"
370	title = "session-surface abstraction; send-keys + send-line injection"
371	required_stages = ["impl", "unit"]
372	[[requirements]]
373	id = "REQ-TERM-3"
374	title = "Byte-stream remote terminal streaming for v1"
375	required_stages = ["impl", "unit"]
376	# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
377	[[requirements]]
378	id = "REQ-TERM-4"
379	title = "Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log"
380	required_stages = ["impl", "unit", "int"]
381	[[requirements]]
382	id = "REQ-FRONT-1"
383	title = "Day-one launcher/manager frontend (list/launch/attach/init)"
384	required_stages = []  # rule 5: GUI milestone — no frontend work in M0–M4
385
386	# ───────────────────────────── Install / migration / infra ──────────────────
387	[[requirements]]
388	id = "REQ-INSTALL-1"
389	title = "Two install paths; signed one-line script; OS-service registration"
390	required_stages = ["doc", "impl", "int"]  # activated M6-D2: installer/install.sh + install.ps1 — the standalone one-line script half (latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → absolute-path print); the harness-bootstrapped half (path a) calls into the same script by design (CONTEXT §Installation). int = oneliner_e2e.rs staged-release rung on both runners. OS-service registration EXCLUDED → docs/DEFERRED.md (daemon auto-start covers dev-stage); "signed" = sha256 at first fetch, full ed25519 is `spt update`'s job (grill decision 3)
391	[[requirements]]
392	id = "REQ-INSTALL-2"
393	title = "Marketplace-repackaging-friendly install"
394	required_stages = ["doc"]  # doc activated M6-D2: the relocatable-binary + minimal non-OS-entangled install-logic stance documented (CONTEXT §Installation tag) — the scripts are that stance (single static binary, no OS service, user-scope PATH only); actual marketplace repackaging stays doc-stage per M6-PLAN §Out
395	[[requirements]]
396	id = "REQ-INSTALL-3"
397	title = "Idempotent + interactive-optional first run"
398	required_stages = ["impl", "int"]  # activated M6-D2: idempotent re-run is construction — binary replaced atomically, PATH registration added at most once (marker-guarded .profile block / HKCU entry-set check); interactive-optional = the scripts never prompt at all (REQ-INSTALL-5's stronger non-interactive contract subsumes it; first-run identity gen + daemon start were already unattended). int = oneliner_e2e re-run rung (second run green, single PATH block)
399	[[requirements]]
400	id = "REQ-INSTALL-4"
401	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"
402	required_stages = ["impl", "unit"]  # activated M5-D2: spt-runtime::registry (validate-first register; copy-for-file_pull/avenue-less vs pointer-for-delegated under {SPT_HOME}/adapters/; soft-deregister retains record+copy, surfaces the manifest uninstall template; re-add re-activates) + spt adapter add/--github/remove/list CLI (install-is-first-update conducted through plan_adapter_update; bounded clone/conduct) + spt-daemon::adapter_update::ripple_registered (the registered set IS the R-UPD-5 ripple source). int = D3 shell-spawn consumes a registered shell adapter E2E
403	[[requirements]]
404	id = "REQ-MIGRATE-1"
405	title = "Auto-detect and migrate a legacy claude_skill_owl install"
406	required_stages = []  # rule 5: post-v1 — migration tooling needs a v1 to migrate to
407	[[requirements]]
408	id = "REQ-INFRA-1"
409	title = "GitHub issue tracking for v1; tangled.org as migration target"
410	required_stages = []  # rule 5: process req, not code — tracked operationally; activates if/when a docs/infra milestone formalizes it
411
412	# ───────────────────────────── M6 stage-setting / release ───────────────────
413	# Added 2026-06-05 (grill `1922fa4`); inactive until M6 execution starts
414	# (rule 5). Activation stages: M6-PLAN.md §Requirement activation.
415	[[requirements]]
416	id = "REQ-INSTALL-5"
417	title = "Non-interactive install path: the canonical one-liner doubles as every adapter's pack-in on-demand install (no second mechanism); sha256-verified fetch; user-PATH registration"
418	required_stages = ["impl", "int"]  # activated M6-D2: install.sh/install.ps1 non-interactive by construction (zero prompts; env knobs only — pipeable `curl|sh` / `irm|iex` cannot take args); SPT_INSTALL_ASSET_BASE is the CI/air-gap fetch override; sha256 verified against the release SHA256SUMS, mismatch refuses before placement. int = oneliner_e2e on both runners incl. the tamper-refusal negative rung
419	[[requirements]]
420	id = "REQ-REL-1"
421	title = "spt-releases publish-target repo: README public face, licensing split, Pages docs at the permanent lapse-proof canonical URL (ADR-0014)"
422	required_stages = ["doc", "impl"]  # doc activated M6-D1: SaberMage/spt-releases created (private, flips at D7) + README per grill decision 7 + LICENSE-MIT/LICENSE-BINARY split w/ royalty-free adapter clause (spt-releases@2debda8); doc tag on ADR-0014. impl activated M6-D5: the publish pipeline — xtask::site assembles the full Pages payload (HTML + raw .md + llms exports + schema + install scripts + rustdoc), docs-publish.yml force-pushes it to the spt-releases gh-pages branch on release tags / dispatch (needs the RELEASES_TOKEN fine-grained PAT secret)
423	[[requirements]]
424	id = "REQ-REL-2"
425	title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
426	required_stages = ["impl", "int"]  # impl activated M6-D6 (release.yml tag-triggered pipeline + xtask release-sign/-publish/-verify); int activated M6-D7: release_verify_e2e.rs — the REAL published v0.1.0's SignedRelease assets fetched from the live release and verified against the EMBEDDED two-key anchor exactly as a deployed node judges a propagated release (env-gated SPT_RELEASE_E2E; run green against the shipped release 2026-06-05, both platforms, key rel-primary-2026). The whole chain proven: tag → CI draft → manual local sign → field binary accepts
427	[[requirements]]
428	id = "REQ-REL-3"
429	title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
430	required_stages = ["impl", "unit"]  # activated M6-D6: BUILTIN_RELEASE_KEYS compiled into every binary (release.rs) with release-keys.json as a pure OVERLAY (file adds/shadows keys; file revocations void builtin ids — per-node compromise recovery without a rebuild; empty builtin set stays fail-closed); ceremony tooling = xtask release-keygen (mints via the spt-proto identity primitive, prints to stdout only, seeds never touch disk/repo/CI) + release-sign (manual local signing off SPT_RELEASE_SEED env). unit = merge/overlay/revoke-builtin + recovery-key-signed-release-verifies + shipped-table-well-formed. The ceremony itself (mint 2 keypairs, embed pubkeys, back up seeds) = maintainer action before the v0.1.0 tag
431
432	# ───────────────────────────── Documentation ────────────────────────────────
433	[[requirements]]
434	id = "REQ-DOCS-1"
435	title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"

# 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"]
[[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 = ["impl", "unit"]  # activated M4-D4c: the broker-owned PresenceLog — a seq'd ring of {connected|disconnected, conn_id, remote_id_hex} events mirroring the StreamLog discipline. register_conn appends connected + spawns a closed-watcher (conn.closed() → conn row removed → disconnected appended), so the conn table finally reflects liveness; the brain subscribes via net-presence-subscribe {from_seq} and consumes net-presence-event under the same contiguous/dedup/gap cursor discipline as streams (brain-restart cursor-resume). The full PresenceChannel *endpoint* (agent-visible channel) builds on this broker seam later; int = two-host at D9.
[[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 = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
[[requirements]]
id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = ["doc", "impl", "unit"]  # doc activated M4-D9-3: docs/DORMANCY-BUDGET.md — the measured warm/cold policy lock (ADR-0003 #9 closed: warm default confirmed, zero idle CPU, RSS-only cost; auto-suspend opt-in default OFF). impl/unit activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md
[[requirements]]
id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant
[[requirements]]
id = "REQ-INST-5"
title = "Two-tier context sync (live to all, project to same-project)"
required_stages = ["impl", "unit", "int"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both directions bootstrap-pull a mind the puller never held, registry-derived want-refs, gate served remotely, HFENDULEAM↔gravity). unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
[[requirements]]
id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = ["impl", "unit", "int"]  # activated M5-D5a/b: the gate is state-keyed at the hook-channel consumer sites — resting::deferred_held reads the owner's durable rest record at drain time; "held" = (deferred=1) ∧ (Dormant|Suspended); poll_drain + cmd_worker_poll gated (the KH 1.4 "all sites agree" sweep — shellchan peek ungated: no deferred writer targets shell perches; ring's ephemeral perch recordless by construction = legacy fallback). Release is by construction at the wake edge: the persisted Active state stops the narrowing, the unchanged delivered-mark discipline keeps it exactly-once. D5b adds the remote arm: StreamFamily::Rest + resthost request/serve (remote-drive trust class — access_check on the handshake-proven origin, no grant gate; refusal = finish-no-reply), cmd_rest qualified-form lift via wansend::wan_rest. unit = held-while-dormant+suspended / non-deferred-untouched / released-once-on-wake / recordless-flows + wire-record roundtrip + demux. int = loopback two-broker E2E (suspend cross-node, gate holds, replay NO_EDGE, wake cross-node releases exactly-once + wake effects, whitelist negative). doc = DEFERRED.md remote-fork note. Rig [twohost] leg waits for D9a.
[[requirements]]
id = "REQ-INST-7"
title = "Subnet registry + bare-id resolution policy"
required_stages = ["impl", "unit", "int"]  # 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 delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both sides' perch rows replicate into the other's gated registry over the real wire). unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
[[requirements]]
id = "REQ-INST-8"
title = "Remote-control mode distinct from local operation"
required_stages = ["impl", "unit", "int"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — A types into B's live PTY across the rig and reads the echo back)
# ── 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 = ["doc", "impl", "unit"]  # activated M4-D9-4: resources: Option<String> on the registry Instance (additive serde-default, rides RegistryUpdate replication + the epoch lease — a blurb edit is an ordinary epoch-bumped update, NO new record kind/store/merge surface) + resource_projection (the (id,node,status,blurb) yellow-pages view over VISIBLE+routable rows — same exclusion closure as resolution, ADR-0006 §6 leak-free by construction) + both-authored sourcing (info.json resources via spt resources set wins; daemon.json resources_blurb node seed fills the gap) + spt resources set/show/list CLI off the stale-tolerant registry snapshots. The whitelist leg of the gate (viewer-node filtering) rides the deferred consent/whitelist generalization — visibility is the v1 gate exactly as CONTEXT scopes it. doc = CONTEXT §resource advertisement; unit = lease-ordered blurb + pre-D9-4 row parses + hidden-never-listed/offline-skipped/absent-renders-clean + both-authored advertisement + runtime refine epoch-bumps
[[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 = ["doc", "impl", "unit"]  # activated M4-D9-5: spt-store::home (assign_home matrix: sole auto / several refuse-and-qualify / unpaired local-only; stamp_creation_fields = the ONE creation seam shared by spt ready + api listen/bind — NEW assigns hard + seeds sync_subnets=[home], REVIVE carries home/adapter/blurb/knobs forward so a re-bind never wipes them; adopt_for_unset = first-join adoption, sole-subnet only, never re-homes; NO setter — immutability by construction) + info.json home_subnet/adapter + ContextStore::fork_endpoint (one-time copy of live+project tiers as parentless seed commits — copied-then-diverged, no shared history) / remove_endpoint (--delete-source, exactly the source) + spt fork CLI (refusal-first gates: same-id, target membership, local + target-registry join-time collision; same-node v1, remote arm = M5 with instantiate-anywhere consent). doc = ADR-0010 delivered note; unit = assignment matrix + stamp news/revives + adoption + fork copies-then-diverges + collision classes + delete-source-exact
[[requirements]]
id = "REQ-REACH-1"
title = "Off-node remote-drive detection + file transfer"
required_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — file fetch off B + push back to B across the rig; chunked xfer through the dispatcher gate)
[[requirements]]
id = "REQ-REACH-2"
title = "Remote command execution (deferred, consent-gated)"
required_stages = []  # rule 5: deferred by design (PRD: consent-gated remote exec is the highest-risk capability; workarounds exist) — M5+ with the instantiate-anywhere consent model

# ───────────────────────────── 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(+--once)/list/stop/whoami, stable arg shapes + exit codes"
required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired. M7 amendment (plan decision 2, 2026-06-05): `poll` removed — it duplicated `ready` (same run_listen dispatch); `ready --once` absorbs the drain-then-exit semantics. `spt api poll`/`api worker-poll` (machinery namespace) are unaffected. Pre-1.0, zero users, no deprecation shim.
[[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
[[requirements]]
id = "REQ-MSG-4"
title = "Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type=\"msg\" from=…> otherwise, chunk oversized lines into EVENT-PART"
required_stages = ["impl", "unit", "int"]  # activated 2026-06-06 (envelope-missing fix): spt-msg::emit render seam + both listener loops (spt ready run_listen, spt api listen cmd_listen); int = quickstart E2E asserts the envelope shape on the live + backlog paths. Hook-channel drains (api poll / worker-poll) stay raw-frame by contract.

# ───────────────────────────── 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", "int"]  # 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. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — a real WAN message lands in the remote perch spool, HFENDULEAM↔gravity over tailscale, [twohost] CI run)
[[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 = ["impl", "unit"]  # activated M4-D6c: git-native bundle sync (ADR-0013). D6c-1 store core: BranchStore bundle plumbing (create_bundle ^have incremental w/ unknown-have full fallback + up-to-date skip; fetch_bundle quarantines tips under refs/spt-sync/ — incoming can never clobber refs/heads before the driver rules; merge_commit_in_worktree two-parent join, FF for all-accepted propagation) + spt_store::syncmerge::apply_fetched — per-file vector verdicts (dominate→accept verbatim/dominated→drop/concurrent→record_conflict, hazard 6.6), replicated .conflicts artifacts with resurrection guard (local-dominates-or-equal ⟹ stale echo dropped), dominating accept clears pending artifacts (the only legal clear), drop-only passes still join the DAG (re-pull short-circuits on ancestry). D6c-2 = wire serve/request over broker QUIC + synced/visibility server gate. int = D9 two-host E2E. unit = adopt/incremental-FF/idempotent-repull + dominated-drop-joins-DAG + concurrent-surfaces-both-nodes + reconciled-write-propagates-clears + p-tier-union + legacy-⊥⊥-surfaces
[[requirements]]
id = "REQ-PAIR-1"
title = "TOTP-seeded SPAKE2 pairing"
required_stages = ["impl", "unit", "int"]  # 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 activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — the real ceremony over SPT_PAIR_ALPN cross-host; trust pinned both sides).
[[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). M7 NOTE: surface moves to `spt subnet show-code [name]` (renamed from show-totp in the M7 UX grill) and creation splits into `spt subnet create <name>` (REQ-SUBNET-1); the gate/resolution semantics here are unchanged. 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+) — delivered by M7 REQ-SUBNET-2 (guided `spt subnet join`); 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", "int"]  # 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 activated M4-D9-6: two-host rig seed transfer green (tests/twohost.rs — the joiner holds the subnet seed post-ceremony); relay rendezvous routing stays the M5 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 = []  # rule 5: GUI milestone (user 2026-06-04) — inline image metadata has no consumer until a GUI exists
# ── M7 subnet & quickstart UX (M7-PLAN.md, grilled 2026-06-05; activate at M7 start, rule 5) ──
[[requirements]]
id = "REQ-SUBNET-1"
title = "spt subnet noun namespace: status view (bare + status [NAME] [--nodes]), create (QR/otpauth), show-code; spt pair deleted"
required_stages = ["impl", "unit"]  # activated M7 D1 (2026-06-05): namespace + create/show-totp ports land; D2 adds the full status views. M7 D1/D2 (plan decisions 3, 5, 6, 11). Bare `spt subnet` = flagless status (names, paired-node counts, endpoint counts; never epochs/codes; hint footer; zero-subnet explanatory text). `status [NAME] [--nodes]` adds per-node rows (label, online/offline via gossip-recency + probe-stale hybrid, online/total endpoints). `create <NAME>` mints (sole seed-holder) + prints code/otpauth/terminal-QR + join hint. `show-code [name]` (nee show-totp) keeps REQ-PAIR-3/6 semantics + now re-provisions the full QR/expiry material. `spt pair` namespace removed outright (no shim).
[[requirements]]
id = "REQ-SUBNET-2"
title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
required_stages = ["impl", "unit", "int"]  # activated M7 D3 (2026-06-05); int added M7 D5: the twohost ladder's pairing rung IS the product surface now — brain pair-join IPC (role A) against the daemon-hosted always-on responder (role B), replacing the test-harness-driven ceremony as the pairing evidence. M7 D3 (plan decisions 8, 10) — the join ceremony's PRODUCT surface (the SPAKE2 wire is rig-proven but test-only today). Every member daemon hosts the pre-trust pairing ALPN responder ALWAYS-ON (ADR-0005 one-sided-UX intent; the red-team #11 subnet-global rate limiter + one-ceremony-per-subnet is the standing-listener guard). `spt subnet join [NAME] [--code <CODE>]`: prompts for missing pieces, LAN+relay rendezvous, clean wrong-code re-prompt (bounded, limiter surfaced honestly), actionable no-seed-holder hints, seed transfer + trust pinning + REQ-INST-15 home adoption on join. int = twohost ladder gains a product-surface join rung (CLI initiator vs daemon responder) replacing the test-harness-driven ceremony as pairing evidence.
[[requirements]]
id = "REQ-SUBNET-3"
title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
required_stages = ["impl", "unit"]  # activated M7 D2 (2026-06-05): label field + hostname stamp + addressable qualifiers land. M7 D2 (plan decision 7). Label defaults to OS hostname, re-checked at daemon startup (hostname change updates it), rides existing registry gossip as an additive serde-default field (old rows parse clean). Pubkey stays the identity; @node qualifiers accept label OR key-prefix; non-unique label resolution refuses-and-qualifies listing candidate key prefixes (CONTEXT.md §node label). Renders `HFENDULEAM (bcead52b…)` in subnet views.
[[requirements]]
id = "REQ-SUBNET-4"
title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): create + join gates live (REQ-PAIR-6 machinery, exit 3, gate-first ordering); ADR-0005 amendment recorded. M7 D3 (plan decision 9). `subnet create` and `subnet join` require OS elevation via the existing REQ-PAIR-6 gate machinery (exit 3 on refusal, no leak/no mutation): an unprivileged process must not mint+leak a subnet secret nor enroll the machine into an attacker's subnet. `subnet status` stays ungated (read-only, no secrets). CONTEXT.md §Pairing & trust updated 2026-06-05.
[[requirements]]
id = "REQ-DOCS-6"
title = "spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)"
required_stages = ["impl", "unit", "int"]  # activated M7 D1 (2026-06-05); int added M7 D5: quickstart_e2e asserts the how-to texts name the commands the published guide's prompt blocks rely on. M7 D1/D4 (plan decision 12). Visible command, Agent-commands help group. Topics v1: `ready` (background-task guidance, --once fallback loop, reply mechanics) + `send` (send/ring, reply-to, SENT vs QUEUED); bare `how-to` lists topics. Single source = the binary — docs-site never duplicates the content (the DOCS-STRATEGY anti-drift posture applied to agent guidance); quickstart prompt blocks instruct agents to run it and follow. int = quickstart_e2e asserts the how-to output names the commands the guide relies on.

# ── 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 = ["impl", "unit"]
# ── 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 = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — insert on A fires on B; dismiss on B replicates back to A; the update consent notif also surfaced through the same primitive)
[[requirements]]
id = "REQ-NOTIF-2"
title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
required_stages = ["doc", "impl", "unit", "int"]  # int activated M5-D9b: the rig toast rung (tests/twohost.rs — A's notify rendered through B's [session.notif] shell template, run 26998058816 real_mode=true)

# ───────────────────────────── Self-update ──────────────────────────────────
[[requirements]]
id = "REQ-UPD-1"
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"]  # activated 2026-06-06: first slice = signed update-set metadata + verification, platform artifact selection, cache/propagation/apply compatibility, maintainer xtask helpers

# ───────────────────────────── 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 = []  # rule 5: GUI milestone — no frontend work in M0–M4

# ───────────────────────────── Install / migration / infra ──────────────────
[[requirements]]
id = "REQ-INSTALL-1"
title = "Two install paths; signed one-line script; OS-service registration"
required_stages = ["doc", "impl", "int"]  # activated M6-D2: installer/install.sh + install.ps1 — the standalone one-line script half (latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → absolute-path print); the harness-bootstrapped half (path a) calls into the same script by design (CONTEXT §Installation). int = oneliner_e2e.rs staged-release rung on both runners. OS-service registration EXCLUDED → docs/DEFERRED.md (daemon auto-start covers dev-stage); "signed" = sha256 at first fetch, full ed25519 is `spt update`'s job (grill decision 3)
[[requirements]]
id = "REQ-INSTALL-2"
title = "Marketplace-repackaging-friendly install"
required_stages = ["doc"]  # doc activated M6-D2: the relocatable-binary + minimal non-OS-entangled install-logic stance documented (CONTEXT §Installation tag) — the scripts are that stance (single static binary, no OS service, user-scope PATH only); actual marketplace repackaging stays doc-stage per M6-PLAN §Out
[[requirements]]
id = "REQ-INSTALL-3"
title = "Idempotent + interactive-optional first run"
required_stages = ["impl", "int"]  # activated M6-D2: idempotent re-run is construction — binary replaced atomically, PATH registration added at most once (marker-guarded .profile block / HKCU entry-set check); interactive-optional = the scripts never prompt at all (REQ-INSTALL-5's stronger non-interactive contract subsumes it; first-run identity gen + daemon start were already unattended). int = oneliner_e2e re-run rung (second run green, single PATH block)
[[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 = ["impl", "unit"]  # activated M5-D2: spt-runtime::registry (validate-first register; copy-for-file_pull/avenue-less vs pointer-for-delegated under {SPT_HOME}/adapters/; soft-deregister retains record+copy, surfaces the manifest uninstall template; re-add re-activates) + spt adapter add/--github/remove/list CLI (install-is-first-update conducted through plan_adapter_update; bounded clone/conduct) + spt-daemon::adapter_update::ripple_registered (the registered set IS the R-UPD-5 ripple source). int = D3 shell-spawn consumes a registered shell adapter E2E
[[requirements]]
id = "REQ-MIGRATE-1"
title = "Auto-detect and migrate a legacy claude_skill_owl install"
required_stages = []  # rule 5: post-v1 — migration tooling needs a v1 to migrate to
[[requirements]]
id = "REQ-INFRA-1"
title = "GitHub issue tracking for v1; tangled.org as migration target"
required_stages = []  # rule 5: process req, not code — tracked operationally; activates if/when a docs/infra milestone formalizes it

# ───────────────────────────── M6 stage-setting / release ───────────────────
# Added 2026-06-05 (grill `1922fa4`); inactive until M6 execution starts
# (rule 5). Activation stages: M6-PLAN.md §Requirement activation.
[[requirements]]
id = "REQ-INSTALL-5"
title = "Non-interactive install path: the canonical one-liner doubles as every adapter's pack-in on-demand install (no second mechanism); sha256-verified fetch; user-PATH registration"
required_stages = ["impl", "int"]  # activated M6-D2: install.sh/install.ps1 non-interactive by construction (zero prompts; env knobs only — pipeable `curl|sh` / `irm|iex` cannot take args); SPT_INSTALL_ASSET_BASE is the CI/air-gap fetch override; sha256 verified against the release SHA256SUMS, mismatch refuses before placement. int = oneliner_e2e on both runners incl. the tamper-refusal negative rung
[[requirements]]
id = "REQ-REL-1"
title = "spt-releases publish-target repo: README public face, licensing split, Pages docs at the permanent lapse-proof canonical URL (ADR-0014)"
required_stages = ["doc", "impl"]  # doc activated M6-D1: SaberMage/spt-releases created (private, flips at D7) + README per grill decision 7 + LICENSE-MIT/LICENSE-BINARY split w/ royalty-free adapter clause (spt-releases@2debda8); doc tag on ADR-0014. impl activated M6-D5: the publish pipeline — xtask::site assembles the full Pages payload (HTML + raw .md + llms exports + schema + install scripts + rustdoc), docs-publish.yml force-pushes it to the spt-releases gh-pages branch on release tags / dispatch (needs the RELEASES_TOKEN fine-grained PAT secret)
[[requirements]]
id = "REQ-REL-2"
title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
required_stages = ["impl", "int"]  # impl activated M6-D6 (release.yml tag-triggered pipeline + xtask release-sign/-publish/-verify); int activated M6-D7: release_verify_e2e.rs — the REAL published v0.1.0's SignedRelease assets fetched from the live release and verified against the EMBEDDED two-key anchor exactly as a deployed node judges a propagated release (env-gated SPT_RELEASE_E2E; run green against the shipped release 2026-06-05, both platforms, key rel-primary-2026). The whole chain proven: tag → CI draft → manual local sign → field binary accepts
[[requirements]]
id = "REQ-REL-3"
title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
required_stages = ["impl", "unit"]  # activated M6-D6: BUILTIN_RELEASE_KEYS compiled into every binary (release.rs) with release-keys.json as a pure OVERLAY (file adds/shadows keys; file revocations void builtin ids — per-node compromise recovery without a rebuild; empty builtin set stays fail-closed); ceremony tooling = xtask release-keygen (mints via the spt-proto identity primitive, prints to stdout only, seeds never touch disk/repo/CI) + release-sign (manual local signing off SPT_RELEASE_SEED env). unit = merge/overlay/revoke-builtin + recovery-key-signed-release-verifies + shipped-table-well-formed. The ceremony itself (mint 2 keypairs, embed pubkeys, back up seeds) = maintainer action before the v0.1.0 tag

# ───────────────────────────── Documentation ────────────────────────────────
[[requirements]]
id = "REQ-DOCS-1"
title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"