<system-reminder>[Truncated: PARTIAL view — showing lines 1-494 of 629 total (27033 tokens, cap 25000). Call Read with offset=495 limit=494 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	roots = ["src", "tests", "docs", "crates"]
14
15	[policy]
16	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle
17	# reqs add "int". Until a req is activated, its per-req `required_stages = []`
18	# keeps it out of the coverage gate.
19	required_stages = []
20
21	# ───────────────────────────── Architecture / workspace ─────────────────────
22	[[requirements]]
23	id = "REQ-ARCH-1"
24	title = "Many small acyclically-layered crates"
25	required_stages = ["impl"]  # activated M0: spt-proto<-spt-store one-way layering (T14). impl-only: architecture is build-evidenced (cargo rejects cycles); grows as crates are added M1+.
26	[[requirements]]
27	id = "REQ-ARCH-2"
28	title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
29	required_stages = ["impl"]
30	[[requirements]]
31	id = "REQ-ARCH-3"
32	title = "Wire-protocol version independent of crate semver, N-1 compat window"
33	required_stages = ["impl", "unit"]  # activated M0: spt-proto wire version + compat window (T6)
34	[[requirements]]
35	id = "REQ-ARCH-4"
36	title = "Copy-verbatim the commodity layer from the sister project"
37	required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT grammar (T2)
38
39	# ───────────────────────────── Daemon ───────────────────────────────────────
40	[[requirements]]
41	id = "REQ-DAEMON-1"
42	title = "One per-machine spt-daemon owning all per-machine state"
43	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) drives the consolidated daemon-hosted lifecycle through the real IPC
44	[[requirements]]
45	id = "REQ-DAEMON-2"
46	title = "Broker/brain split for seamless self-update"
47	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves a brain kill+restart leaves the broker's PTY child + output stream intact and gapless (cross-cut with handoff.rs)
48	[[requirements]]
49	id = "REQ-DAEMON-3"
50	title = "Any api invocation auto-starts the daemon if absent"
51	required_stages = ["impl", "unit", "int"]
52	[[requirements]]
53	id = "REQ-DAEMON-4"
54	title = "Honor every KNOWN-HAZARDS invariant"
55	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves graceful signoff runs the echo-before-teardown ordering (3.3) and removes the Self's ready marker in the consolidated daemon
56
57	# ───────────────────────────── Storage ──────────────────────────────────────
58	# git-KV BranchStore substrate (ADR-0011, 2026-06-02; implement M4, optional M3c)
59	[[requirements]]
60	id = "REQ-STORE-1"
61	title = "spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)"
62	required_stages = ["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
63
64	# ───────────────────────────── Harness contract — manifest seams ────────────
65	[[requirements]]
66	id = "REQ-MANIFEST-1"
67	title = "Per-adapter manifest with adapter_name and min_spt_core_version"
68	required_stages = ["doc", "impl", "unit"]
69	[[requirements]]
70	id = "REQ-SEAM-SPAWN"
71	title = "spawn-session seam"
72	required_stages = ["impl", "unit"]
73	[[requirements]]
74	id = "REQ-SEAM-POSTSPAWN"
75	title = "post-spawn / api bind seam with boot nonce"
76	required_stages = ["impl", "unit"]
77	[[requirements]]
78	id = "REQ-SEAM-PSYCHE"
79	title = "spawn-psyche seam (fresh + resume templates)"
80	required_stages = ["impl", "unit", "int"]
81	[[requirements]]
82	id = "REQ-SEAM-HISTORY"
83	title = "History subsystem (fetcher / locate-normalize / native store)"
84	required_stages = ["impl", "unit", "int"]
85	[[requirements]]
86	id = "REQ-SEAM-ACTIVITY"
87	title = "Activity/idle reported via api sentinels, not PTY quiescence"
88	required_stages = ["impl", "unit"]
89	[[requirements]]
90	id = "REQ-SEAM-INJECT"
91	title = "inject-input methods configurable per activity-state"
92	required_stages = ["impl", "unit"]
93	[[requirements]]
94	id = "REQ-SEAM-RESUME"
95	title = "resume-session seam (fresh-with-preload / continue-existing)"
96	required_stages = ["impl", "unit"]
97	[[requirements]]
98	id = "REQ-SEAM-CAPABILITY"
99	title = "Hostable endpoint-types capability declaration"
100	required_stages = ["impl", "unit"]
101	[[requirements]]
102	id = "REQ-SEAM-UPDATE"
103	title = "Adapter-update avenue (file-pull / delegated command)"
104	required_stages = ["impl", "unit"]  # activated M3c-C3: manifest [update] gains signing_key (file_pull content key) + self_verifies (delegated attest); adapter_update::plan_adapter_update dispatches each avenue (file_pull verified against the adapter key, delegated delegated-or-skipped)
105
106	# ───────────────────────────── Harness contract — api surface ───────────────
107	[[requirements]]
108	id = "REQ-API-1"
109	title = "api prefix and adapter_name on every machinery invocation"
110	required_stages = ["impl", "unit", "int"]
111	[[requirements]]
112	id = "REQ-API-2"
113	title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
114	required_stages = ["impl", "unit", "int"]
115	[[requirements]]
116	id = "REQ-API-3"
117	title = "commune/signoff are file-drops, not commands"
118	required_stages = ["impl", "unit", "int"]
119
120	# ───────────────────────────── Startup flows ────────────────────────────────
121	[[requirements]]
122	id = "REQ-START-1"
123	title = "Adapters never resolve SPT_HOME; binary on PATH; api bridging only"
124	required_stages = ["impl", "unit"]
125	[[requirements]]
126	id = "REQ-START-2"
127	title = "Harness-hosted startup: api seed then listen"
128	required_stages = ["impl", "unit", "int"]
129	[[requirements]]
130	id = "REQ-START-3"
131	title = "spt-hosted startup: spawn-session then api bind (no file)"
132	required_stages = ["impl", "unit", "int"]
133	[[requirements]]
134	id = "REQ-START-4"
135	title = "Adapter-injected env aliases (SPT/OWL/LIVE)"
136	required_stages = ["impl", "unit"]
137
138	# ───────────────────────────── Endpoints / Shells ───────────────────────────
139	[[requirements]]
140	id = "REQ-EP-1"
141	title = "Day-one endpoint types; open type system"
142	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
143	[[requirements]]
144	id = "REQ-EP-2"
145	title = "Agent endpoints vs Shells distinction in the type model"
146	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
147	[[requirements]]
148	id = "REQ-EP-3"
149	title = "Messaging payloads carry typed operation commands + file blobs"
150	required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
151	[[requirements]]
152	id = "REQ-EP-4"
153	title = "PresenceChannel broker endpoint (seam day-one)"
154	required_stages = ["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.
155	[[requirements]]
156	id = "REQ-EP-5"
157	title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
158	required_stages = ["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
159
160	# ───────────────────────────── Instances ────────────────────────────────────
161	[[requirements]]
162	id = "REQ-INST-1"
163	title = "endpoint ID vs instance split (adapter-agnostic ID)"
164	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
165	[[requirements]]
166	id = "REQ-INST-2"
167	title = "Per-node files, synced Psyche mind"
168	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
169	[[requirements]]
170	id = "REQ-INST-3"
171	title = "Dormant (warm) / suspended (cold) resting states"
172	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
173	[[requirements]]
174	id = "REQ-INST-4"
175	title = "active to dormant/suspended fires a transition echo commune"
176	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
177	[[requirements]]
178	id = "REQ-INST-5"
179	title = "Two-tier context sync (live to all, project to same-project)"
180	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)
181	[[requirements]]
182	id = "REQ-INST-6"
183	title = "Deferred messages not delivered to dormant/suspended instances"
184	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.
185	[[requirements]]
186	id = "REQ-INST-7"
187	title = "Subnet registry + bare-id resolution policy"
188	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).
189	[[requirements]]
190	id = "REQ-INST-8"
191	title = "Remote-control mode distinct from local operation"
192	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)
193	# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
194	[[requirements]]
195	id = "REQ-INST-9"
196	title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
197	required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
198	[[requirements]]
199	id = "REQ-INST-10"
200	title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
201	required_stages = ["impl", "unit"]  # activated M4-D3c: spt_proto::addr::Address::parse (grammar, reserved :/@ → unambiguous split, each component charset-validated) + spt_net::net::registry::resolve/resolve_across (policy: @node exact → local wins → sole live → REFUSE on multiple live nodes [AcrossNodes, force @node] / multiple subnets [AcrossSubnets, force subnet:]; Offline never routed). Per-node epochs are NOT cross-node comparable, so no silent most-recently-active guess. unit = addr 4-shape parse/roundtrip/doubled-delim reject + resolve local/sole/refuse/@node-pin/cross-subnet. Cross-node replication feeding the registry = D4; two-host = D9.
202	[[requirements]]
203	id = "REQ-INST-11"
204	title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
205	required_stages = ["impl", "unit"]  # activated M4-D3f: three local arms. (1) registry rows: spt_net::net::registry::rename_endpoint — collision-checked against EVERY subnet the endpoint is advertised into FIRST (any row, any status holds the name), then moved all-or-nothing; rows verbatim (relabel, not liveness event); new id charset-validated (:/@ reserved). (2) perch dirs: spt_store::rename::rename_perch_in — Self dir + nested <id>-psyche/<id>-w{N} children + every info.json id; refuses while any perch in the tree is live; local SQLite registry rows deliberately untouched (live refuses ⇒ old rows stale ⇒ 4.3 stale-clean drops). (3) CLI `spt rename <old> <new>` wires the perch arm. Seams: daemon-held registry wiring = D4; a-<id> context branch = D6 context store; cross-node ripple = authored (node,epoch) op reconciled newest-wins per hazard 6.5 marker at D4/D6. unit = cross-subnet ripple-verbatim + all-or-nothing collision/NotFound/InvalidId/self-noop + perch tree ripple + refusals-untouched + live-child-blocks + CLI parse.
206	[[requirements]]
207	id = "REQ-INST-12"
208	title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
209	required_stages = ["impl", "unit"]  # activated M4-D3e: spt_store::visibility::VisibilityStore (identity/visibility.json, atomic, degrade-safe) — hidden(E,S,flag) = per-(E,S) override wins both directions, else S.hide_new_endpoints OR E.default_hide (both ship OFF). SubnetRecord.hide_new_endpoints serde-defaulted (legacy subnet.json loads OFF). Enforcement in spt_net::net::registry: excluded = not advertised (advertise_if_visible gates BEFORE the collision check — hidden neither claims nor clashes) AND not routable (resolve_visible/resolve_across_visible refuse even @node pins; exclusion prunes the cross-subnet ambiguity set). unit = OR-of-defaults + override-both-directions + persist/degrade + legacy-load + not-routable + ambiguity-prune + advertise-gate.
210	[[requirements]]
211	id = "REQ-INST-13"
212	title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
213	required_stages = ["impl", "unit"]  # activated M4-D3e: VisibilityStore.sync_subnets per-endpoint membership list + synced(E,S,flag) — the D6 replication gate: subnet must be on the list (unconfigured list syncs NOWHERE, fail-closed; creation flow seeds [home] per ADR-0010) AND visibility gates sync (hidden ⟹ not synced even when listed, ADR-0006 §6). Actual mind replication consuming this gate = D6. unit = listed-visible-syncs + unlisted/unconfigured-no + hidden-gates-sync (defaults + override paths).
214	[[requirements]]
215	id = "REQ-INST-14"
216	title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
217	required_stages = ["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
218	[[requirements]]
219	id = "REQ-INST-15"
220	title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
221	required_stages = ["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
222	[[requirements]]
223	id = "REQ-REACH-1"
224	title = "Off-node remote-drive detection + file transfer"
225	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)
226	[[requirements]]
227	id = "REQ-REACH-2"
228	title = "Remote command execution (deferred, consent-gated)"
229	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
230
231	# ───────────────────────────── Local messaging (M1) ─────────────────────────
232	# No PRD R-* covers local message delivery (foundational/implicit). Registered
233	# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
234	# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
235	[[requirements]]
236	id = "REQ-MSG-1"
237	title = "Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)"
238	required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
239	[[requirements]]
240	id = "REQ-MSG-2"
241	title = "spt binary CLI surface: send/ring/ready/poll/list/stop/whoami, stable arg shapes + exit codes"
242	required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired
243	[[requirements]]
244	id = "REQ-MSG-3"
245	title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
246	required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E
247
248	# ───────────────────────────── Networking / pairing ─────────────────────────
249	[[requirements]]
250	id = "REQ-NODE-IDENTITY"
251	title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
252	required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
253	[[requirements]]
254	id = "REQ-NET-1"
255	title = "WAN messaging first-class, behind default-on net feature flag"
256	required_stages = ["impl", "unit", "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)
257	[[requirements]]
258	id = "REQ-NET-2"
259	title = "n0 relay default + self-host knob + plain-language disclosure"
260	required_stages = ["impl"]  # activated M4-D1: RelayPolicy {N0Default | SelfHosted(RelayMap) | Disabled} maps onto iroh RelayMode (n0 public relays are the zero-config default; self-host is the escape hatch; disabled = LAN/air-gapped + hermetic tests), plus the RELAY_DISCLOSURE plain-language string. Config plumbing — no unit/int stage required
261	[[requirements]]
262	id = "REQ-NET-3"
263	title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
264	required_stages = ["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
265	[[requirements]]
266	id = "REQ-PAIR-1"
267	title = "TOTP-seeded SPAKE2 pairing"
268	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).
269	[[requirements]]
270	id = "REQ-PAIR-2"
271	title = "Local trust store with TOFU + warn-on-change"
272	required_stages = ["impl", "unit"]  # activated M4-D2e: spt-store::trust::TrustStore (identity/trust/peers.json) pins peer node pubkeys per-subnet post-ceremony (TOFU). record() => Pinned/AlreadyTrusted/KeyChanged{existing}; KeyChanged never auto-applied (repin on accepted rotation); is_trusted = connect gate; revoke = trust-delete (pair w/ seed rotation #10). Warn-on-change anchored on optional (subnet,user,label) — dormant until labels exist (GUI/D9), honest v1 residual. int = on-wire trusted-vs-untrusted gate at D2-wire/D9.
273	[[requirements]]
274	id = "REQ-PAIR-3"
275	title = "Fetch current pairing code from any paired node"
276	required_stages = ["impl", "unit"]  # activated M4-D2g: `spt pair show-totp [--subnet <name>|--create-new <name>]` surfaces a subnet's current code + otpauth:// URI off this node (cli.rs decide_show_totp). Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
277	[[requirements]]
278	id = "REQ-PAIR-4"
279	title = "Subnet naming on first pairing"
280	required_stages = ["impl", "unit"]  # activated M4-D2f: SubnetStore::create_subnet names the subnet at creation (sole seed-holder, epoch 1; "naming moves to link-start" ADR-0006); the name rides every ceremony (joiner types it, transcript binds it) and reaches the joiner via D2f seed transfer. unit = wire create_new_then_join_learns_named_subnet. CLI naming prompt is later (daemon wiring, D4+); int = two-host at D9.
281	# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
282	[[requirements]]
283	id = "REQ-PAIR-5"
284	title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
285	required_stages = ["impl", "unit", "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.
286	[[requirements]]
287	id = "REQ-PAIR-6"
288	title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
289	required_stages = ["impl", "unit"]  # activated M4-D2g: elevation.rs probes OS privilege (Linux libc::geteuid()==0; Windows GetTokenInformation(TokenElevation) via windows-sys; cfg-split) — only the binary pulls OS deps. gate_for: Elevated => Show, NotElevated|Unknown => Fallback (fail safe, never leak the code unconfirmed). Refused fetch leaks no code, persists no mint, distinct exit 3, and the fallback names the authenticator app (seed provisioned there at pairing). unit = gate decision both ways + Unknown fallback + no-leak/no-save on refusal; the live OS probe is environment-dependent (manual verification via todlando / D9). The elevated-*agent*-endpoint path (IPC) is D4/D5, not D2g.
290	[[requirements]]
291	id = "REQ-PAIR-7"
292	title = "Subnet icon (inline image metadata, GUI-only consumer)"
293	required_stages = []  # rule 5: GUI milestone (user 2026-06-04) — inline image metadata has no consumer until a GUI exists
294	# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
295	[[requirements]]
296	id = "REQ-SEC-1"
297	title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
298	required_stages = ["impl", "unit"]
299	# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
300	[[requirements]]
301	id = "REQ-NOTIF-1"
302	title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
303	required_stages = ["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)
304	[[requirements]]
305	id = "REQ-NOTIF-2"
306	title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
307	required_stages = ["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)
308
309	# ───────────────────────────── Self-update ──────────────────────────────────
310	[[requirements]]
311	id = "REQ-UPD-1"
312	title = "Peer-propagated update over P2P"
313	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)
314	[[requirements]]
315	id = "REQ-UPD-2"
316	title = "All binaries signature-verified before handoff"
317	required_stages = ["impl", "unit"]  # activated M3c-C1: release.rs Ed25519 verify-before-handoff (signature + key trust + SHA-256 artifact binding); update::plan_verified is the front door that produces a plan ONLY for a verified release, so an unverified binary never reaches apply_brain_only
318	[[requirements]]
319	id = "REQ-UPD-3"
320	title = "No endpoint process terminates/suspends during self-update"
321	required_stages = ["impl", "unit", "int"]  # activated M3c-C0: the update engine's brain-only apply swaps logic over the B2 handoff substrate; the C0 E2E (tests/update.rs) proves a live PTY child's pid is unchanged + its output stream gapless across the swap (zero endpoint interruption — ADR-0004 §A, absolute for the brain-only class)
322	[[requirements]]
323	id = "REQ-UPD-4"
324	title = "Update gated on user confirmation by default; opt-in full-auto"
325	required_stages = ["impl", "unit"]  # activated M3c-C2: consent::decide is the pure gate (default NeedsConsent, full-auto opt-in via DaemonConfig.full_auto_update); consent::most_recently_active resolves the prompt target over live perches + the new info.json last_active_ms recency stamp (set_last_active, stamped each pulse tick). Interactive prompt UX itself rides the deferred notif/PresenceChannel seam
326	[[requirements]]
327	id = "REQ-UPD-5"
328	title = "spt-core ripple-updates registered adapters"
329	required_stages = ["impl", "unit"]  # activated M3c-C3: adapter_update::conduct_ripple conducts self-then-adapters in registration order; file_pull payloads get adapter content signing (verify_signature against the per-adapter manifest key + verify_artifact digest), delegated requires a self_verifies attest
330
331	# ───────────────────────────── Terminal wrapper / frontend ──────────────────
332	[[requirements]]
333	id = "REQ-TERM-1"
334	title = "Process-supervisor terminal wrapper hosting broker PTYs"
335	required_stages = ["impl", "unit"]
336	[[requirements]]
337	id = "REQ-TERM-2"
338	title = "session-surface abstraction; send-keys + send-line injection"
339	required_stages = ["impl", "unit"]
340	[[requirements]]
341	id = "REQ-TERM-3"
342	title = "Byte-stream remote terminal streaming for v1"
343	required_stages = ["impl", "unit"]
344	# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
345	[[requirements]]
346	id = "REQ-TERM-4"
347	title = "Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log"
348	required_stages = ["impl", "unit", "int"]
349	[[requirements]]
350	id = "REQ-FRONT-1"
351	title = "Day-one launcher/manager frontend (list/launch/attach/init)"
352	required_stages = []  # rule 5: GUI milestone — no frontend work in M0–M4
353
354	# ───────────────────────────── Install / migration / infra ──────────────────
355	[[requirements]]
356	id = "REQ-INSTALL-1"
357	title = "Two install paths; signed one-line script; OS-service registration"
358	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D2 — script halves; OS-service stays deferred)
359	[[requirements]]
360	id = "REQ-INSTALL-2"
361	title = "Marketplace-repackaging-friendly install"
362	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D2)
363	[[requirements]]
364	id = "REQ-INSTALL-3"
365	title = "Idempotent + interactive-optional first run"
366	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D2)
367	[[requirements]]
368	id = "REQ-INSTALL-4"
369	title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"
370	required_stages = ["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
371	[[requirements]]
372	id = "REQ-MIGRATE-1"
373	title = "Auto-detect and migrate a legacy claude_skill_owl install"
374	required_stages = []  # rule 5: post-v1 — migration tooling needs a v1 to migrate to
375	[[requirements]]
376	id = "REQ-INFRA-1"
377	title = "GitHub issue tracking for v1; tangled.org as migration target"
378	required_stages = []  # rule 5: process req, not code — tracked operationally; activates if/when a docs/infra milestone formalizes it
379
380	# ───────────────────────────── M6 stage-setting / release ───────────────────
381	# Added 2026-06-05 (grill `1922fa4`); inactive until M6 execution starts
382	# (rule 5). Activation stages: M6-PLAN.md §Requirement activation.
383	[[requirements]]
384	id = "REQ-INSTALL-5"
385	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"
386	required_stages = []  # activates at M6 start (D2)
387	[[requirements]]
388	id = "REQ-REL-1"
389	title = "spt-releases publish-target repo: README public face, licensing split, Pages docs at the permanent lapse-proof canonical URL (ADR-0014)"
390	required_stages = []  # activates at M6 start (D1/D5)
391	[[requirements]]
392	id = "REQ-REL-2"
393	title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
394	required_stages = []  # activates at M6 start (D6/D7)
395	[[requirements]]
396	id = "REQ-REL-3"
397	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)"
398	required_stages = []  # activates at M6 start (D6)
399
400	# ───────────────────────────── Documentation ────────────────────────────────
401	[[requirements]]
402	id = "REQ-DOCS-1"
403	title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
404	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D4/D5 — DOCS-STRATEGY §v0.1 Tier 1)
405	[[requirements]]
406	id = "REQ-DOCS-2"
407	title = "Sub-10-minute runnable killer quickstart per audience"
408	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
409	[[requirements]]
410	id = "REQ-DOCS-3"
411	title = "Diátaxis structure; one canonical way to do X"
412	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
413	[[requirements]]
414	id = "REQ-DOCS-4"
415	title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
416	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
417	[[requirements]]
418	id = "REQ-DOCS-5"
419	title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
420	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
421
422	# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
423	# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
424	# `unit` (and `int` where cross-process/cross-node) — the conformance checklist
425	# becomes a CI gate (PRD success criterion #6).
426
427	[[requirements]]
428	id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
429	title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
430	required_stages = ["impl", "unit"]
431	[[requirements]]
432	id = "REQ-HAZARD-INFO-JSON-TORN-READ"
433	title = "State-file reads tolerate concurrent writes (1.2)"
434	required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
435	[[requirements]]
436	id = "REQ-HAZARD-STALE-INDEX-LOCK"
437	title = "Sweep stale lockfiles on daemon boot (1.3)"
438	required_stages = ["impl", "unit"]  # activated M5-D3e: branchstore::sweep_stale_index_locks (0-byte + age-gated, seed git-dir + worktree git-dirs) called from Daemon::run boot housekeeping
439	[[requirements]]
440	id = "REQ-HAZARD-DEFERRED-DRAIN"
441	title = "Deferred spool rows excluded from the event-stream drain (1.4)"
442	required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
443	[[requirements]]
444	id = "REQ-HAZARD-WORKER-PATH"
445	title = "Single source of truth for Worker/Psyche perch location (1.5)"
446	required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
447	[[requirements]]
448	id = "REQ-HAZARD-PARENT-PID-PREFER"
449	title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
450	required_stages = []  # rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)
451	[[requirements]]
452	id = "REQ-HAZARD-STDIN-SESSION-ID"
453	title = "Stdin session_id precedence over env (2.2)"
454	required_stages = []  # rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such path exists in spt-core; re-assessed M5-D3e: the whole shell api surface passes identity as argv (--link token, positional shell-id), no stdin id seam grew through D3
455	[[requirements]]
456	id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
457	title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
458	required_stages = ["impl", "unit"]
459	[[requirements]]
460	id = "REQ-HAZARD-GEN-START-NOW"
461	title = "gen_start = now() on cold-start and handoff (2.4)"
462	required_stages = ["impl", "int"]
463	[[requirements]]
464	id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
465	title = "Ephemeral perch cleanup on every ring exit path (3.1)"
466	required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
467	[[requirements]]
468	id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
469	title = "Stale signoff sentinel does not kill a fresh start (3.2)"
470	required_stages = ["impl", "unit"]
471	[[requirements]]
472	id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
473	title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
474	required_stages = ["impl", "unit"]
475	[[requirements]]
476	id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
477	title = "Envelope decode order, ampersand decoded last (4.1)"
478	required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec
479	[[requirements]]
480	id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
481	title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
482	required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
483	[[requirements]]
484	id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
485	title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
486	required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3)
487	[[requirements]]
488	id = "REQ-HAZARD-ID-CHARSET"
489	title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
490	required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
491	[[requirements]]
492	id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
493	title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
494	required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)

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

[scan]
roots = ["src", "tests", "docs", "crates"]

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

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

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

# ───────────────────────────── Storage ──────────────────────────────────────
# git-KV BranchStore substrate (ADR-0011, 2026-06-02; implement M4, optional M3c)
[[requirements]]
id = "REQ-STORE-1"
title = "spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)"
required_stages = ["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/poll/list/stop/whoami, stable arg shapes + exit codes"
required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired
[[requirements]]
id = "REQ-MSG-3"
title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E

# ───────────────────────────── Networking / pairing ─────────────────────────
[[requirements]]
id = "REQ-NODE-IDENTITY"
title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
[[requirements]]
id = "REQ-NET-1"
title = "WAN messaging first-class, behind default-on net feature flag"
required_stages = ["impl", "unit", "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). Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
[[requirements]]
id = "REQ-PAIR-4"
title = "Subnet naming on first pairing"
required_stages = ["impl", "unit"]  # activated M4-D2f: SubnetStore::create_subnet names the subnet at creation (sole seed-holder, epoch 1; "naming moves to link-start" ADR-0006); the name rides every ceremony (joiner types it, transcript binds it) and reaches the joiner via D2f seed transfer. unit = wire create_new_then_join_learns_named_subnet. CLI naming prompt is later (daemon wiring, D4+); int = two-host at D9.
# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-PAIR-5"
title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
required_stages = ["impl", "unit", "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
# ── 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

# ───────────────────────────── 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 = []  # rule 5: activates at M6 start (M6-PLAN.md D2 — script halves; OS-service stays deferred)
[[requirements]]
id = "REQ-INSTALL-2"
title = "Marketplace-repackaging-friendly install"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D2)
[[requirements]]
id = "REQ-INSTALL-3"
title = "Idempotent + interactive-optional first run"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D2)
[[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 = []  # activates at M6 start (D2)
[[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 = []  # activates at M6 start (D1/D5)
[[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 = []  # activates at M6 start (D6/D7)
[[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 = []  # activates at M6 start (D6)

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

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

[[requirements]]
id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-INFO-JSON-TORN-READ"
title = "State-file reads tolerate concurrent writes (1.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
[[requirements]]
id = "REQ-HAZARD-STALE-INDEX-LOCK"
title = "Sweep stale lockfiles on daemon boot (1.3)"
required_stages = ["impl", "unit"]  # activated M5-D3e: branchstore::sweep_stale_index_locks (0-byte + age-gated, seed git-dir + worktree git-dirs) called from Daemon::run boot housekeeping
[[requirements]]
id = "REQ-HAZARD-DEFERRED-DRAIN"
title = "Deferred spool rows excluded from the event-stream drain (1.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
[[requirements]]
id = "REQ-HAZARD-WORKER-PATH"
title = "Single source of truth for Worker/Psyche perch location (1.5)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-PARENT-PID-PREFER"
title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
required_stages = []  # rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)
[[requirements]]
id = "REQ-HAZARD-STDIN-SESSION-ID"
title = "Stdin session_id precedence over env (2.2)"
required_stages = []  # rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such path exists in spt-core; re-assessed M5-D3e: the whole shell api surface passes identity as argv (--link token, positional shell-id), no stdin id seam grew through D3
[[requirements]]
id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-GEN-START-NOW"
title = "gen_start = now() on cold-start and handoff (2.4)"
required_stages = ["impl", "int"]
[[requirements]]
id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
title = "Ephemeral perch cleanup on every ring exit path (3.1)"
required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
[[requirements]]
id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
title = "Stale signoff sentinel does not kill a fresh start (3.2)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
[[requirements]]
id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3)
[[requirements]]
id = "REQ-HAZARD-ID-CHARSET"
title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)


PowerShell

gh auth status 2>&1 | Select-Object -First 6; gh repo view SaberMage/spt-releases --json name,visibility 2>&1

Check gh auth and whether spt-releases repo exists


Error: Exit code 1
github.com
  ✓ Logged in to github.com account SaberMage (keyring)
  - Active account: true
  - Git operations protocol: https
  - Token: gho_************************************
  - Token scopes: 'admin:org_hook', 'admin:repo_hook', 'gist', 'project', 'read:audit_log', 'read:discussion', 'read:user', 'repo', 'workflow', 'write:org'
GraphQL: Could not resolve to a Repository with the name 'SaberMage/spt-releases'. (repository)


Error: File does not exist. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.


No files found


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


1	# Publish-target repo + lapse-proof canonical URL
2
3	The `spt-core` source repo stays **private long-term** (clean-room rebuild; internal planning corpus). All dev-facing artifacts publish to a separate repo, **`SaberMage/spt-releases`** — README, GitHub Releases (binaries + dev assets), and the rendered dev docs via GitHub Pages. It is a pure publish target: doc truth and CI doc-generation live in the source repo; a pipeline pushes rendered output. Created private; flips public anytime (Pages goes live at the flip — no Pro plan).
4
5	**Canonical URL = `https://sabermage.github.io/spt-releases`, with no custom domain configured on Pages.** This URL is what gets baked everywhere: docs cross-links, `llms.txt`, the install one-liner in the README, and every adapter's pack-in install logic.
6
7	## Considered options
8
9	- **Custom domain (`spt-core.decidel.com`) as Pages canonical** — rejected. Configuring a Pages custom domain makes GitHub redirect all github.io URLs to it, so the domain becomes canonical for *everything*, including the install one-liner baked into third-party adapters. A lapsed/transferred domain then hands arbitrary code execution (`curl | sh`) to whoever registers it next. The github.io URL is account-bound, free, and cannot lapse.
10	- `spt-core.decidel.com` survives only as an optional **human-facing redirect → Pages** (marketing/vanity). It never appears in install commands, adapter code, or doc cross-links.
11
12	## Consequences
13
14	- Install scripts are served from the Pages URL (not `raw.githubusercontent.com`), keeping hosting swappable behind the stable URL.
15	- DOCS-STRATEGY principle 9 (never-renamed URLs; agents cache links) is anchored to the github.io origin permanently.
16	- Everything authored in `spt-releases` must be public-safe from day one.
17

# Publish-target repo + lapse-proof canonical URL

The `spt-core` source repo stays **private long-term** (clean-room rebuild; internal planning corpus). All dev-facing artifacts publish to a separate repo, **`SaberMage/spt-releases`** — README, GitHub Releases (binaries + dev assets), and the rendered dev docs via GitHub Pages. It is a pure publish target: doc truth and CI doc-generation live in the source repo; a pipeline pushes rendered output. Created private; flips public anytime (Pages goes live at the flip — no Pro plan).

**Canonical URL = `https://sabermage.github.io/spt-releases`, with no custom domain configured on Pages.** This URL is what gets baked everywhere: docs cross-links, `llms.txt`, the install one-liner in the README, and every adapter's pack-in install logic.

## Considered options

- **Custom domain (`spt-core.decidel.com`) as Pages canonical** — rejected. Configuring a Pages custom domain makes GitHub redirect all github.io URLs to it, so the domain becomes canonical for *everything*, including the install one-liner baked into third-party adapters. A lapsed/transferred domain then hands arbitrary code execution (`curl | sh`) to whoever registers it next. The github.io URL is account-bound, free, and cannot lapse.
- `spt-core.decidel.com` survives only as an optional **human-facing redirect → Pages** (marketing/vanity). It never appears in install commands, adapter code, or doc cross-links.

## Consequences

- Install scripts are served from the Pages URL (not `raw.githubusercontent.com`), keeping hosting swappable behind the stable URL.
- DOCS-STRATEGY principle 9 (never-renamed URLs; agents cache links) is anchored to the github.io origin permanently.
- Everything authored in `spt-releases` must be public-safe from day one.