1	# M4 Plan — networking + instances
2
3	> **Status: COMPLETE (2026-06-04).** D0–D9 all delivered CI-green; the §D9b
4	> two-host proof ran on the real rig (CI run `26958175812`, HFENDULEAM ↔
5	> gravity-linux — `docs/TWO-HOST-RUNBOOK.md` evidence appendix); every M4 req
6	> satisfied at its `required_stages` or rule-5-deferred with rationale
7	> (`traceable-reqs check` green); ROADMAP §M4 records the delivery. Deferred
8	> seams annotated to M5/post-v1 in ROADMAP §M5 + §Out below.
9
10	> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a/M3b/M3c-PLAN.md`.
11	> Task layer authored in full 2026-06-03 at M4 start (see §Tasks).
12	> Branch: `dev-freeform`. Authoritative architecture: **ADR-0002** (networking baked
13	> into core), **ADR-0003** (multi-instance node-anchored identity), **ADR-0005**
14	> (pairing), **ADR-0006/0007** (subnet membership + notification primitive), and
15	> `ROADMAP.md` §M4.
16
17	> **Upstream is done:** **M3 COMPLETE (2026-06-03)** — M3a (spt-term PTY) · M3b
18	> (broker/brain daemon) · M3c (signed self-update). The broker's resource-ownership
19	> table (ADR-0004 §B) and the update engine's transport seam are both built
20	> transport-agnostic precisely so M4 is an *integration*, not a re-architecture:
21	> M4 plugs the P2P transport into seams that already exist.
22
23	## Goal
24
25	Deliver the real cross-machine proof: `spt-net` (Iroh WAN + mDNS LAN + TOTP-SPAKE2
26	pairing + the eventually-consistent subnet registry) and the multi-instance model
27	(ADR-0003) so the same agent runs on each of a user's machines with a synced mind,
28	reachable and operable from anywhere — **cross-node message + drive an agent on
29	machine A from machine B**, plus update peer-propagation across the subnet.
30
31	## Scope
32
33	### In
34
35	- **`spt-net` crate** — the remaining sibling in the layering
36	  (`…→spt-msg→{spt-net, spt-term, spt-runtime}→…`): Iroh endpoint (WAN, the Spike #2
37	  shape) + mDNS (LAN discovery) + TOTP-SPAKE2 pairing (ADR-0005) (`R-NET-*`, `R-PAIR-*`).
38	- **Subnet registry** — eventually-consistent `endpoint_id → [instances]` + resolution
39	  policy (local → most-recently-active → `id@node`) (`REQ-INST-7`).
40	- **QUIC-stream broker ownership — the implementation.** Spike #3 validated the
41	  *shape* (a broker-owned Iroh endpoint + live QUIC stream survives a brain restart);
42	  M4 builds it for real into the broker's ADR-0004 §B ownership table.
43	- **Cross-node Psyche sync (P2P)** — replaces the interim gh-repo sync; the direct-write
44	  precedence marker + node id (KNOWN-HAZARDS 6.5).
45	- **Remote-drive of an already-running instance** — the network session-surface (the
46	  highest-value v1 slice; remote-drive detection + off-node file transfer).
47	- **Update peer-propagation transport (`REQ-UPD-1`)** — plug the P2P transport into the
48	  **M3c update-engine seam**: the subnet self-heals to latest, every propagated binary
49	  still passing the M3c signature/rollback gate before any handoff (one compromised node
50	  must not poison the subnet — ADR-0004 §D).
51
52	### Out
53
54	- **Shells, PresenceChannel, and the consent-gated capabilities** (remote-exec,
55	  instantiate-anywhere) — **M5**, landing into the v1 consent seam M3c-C2 already shaped.
56	- **Cross-user / per-(subnet, user)** generalization of the notif spool + dismissal — a
57	  documented forward seam, not v1.
58
59	## Requirements (activate per task when M4 starts)
60
61	`R-NET-*`, `R-PAIR-*`, `REQ-INST-7`, `REQ-UPD-1`, `REQ-EP-4` (PresenceChannel impl — or
62	M5), and the distributed precedence/freshness generalization of 6.5 — all currently
63	`required_stages = []`. Re-confirm the new-requirement assessment (TRACEABILITY rule 3)
64	at task-authoring time (pairing + subnet wire formats may surface `REQ-HAZARD-*`).
65
66	## Clean-room posture
67
68	Copy-verbatim only the sister's stable wire/registry **formats** that survive (ADR-0001);
69	clean-room the transport (the sister had no P2P). Reuse Spike #2/#3 Iroh learnings (iroh
70	0.98 API, the loopback QUIC-survival shape) — those were sandbox spikes, not in-repo crates.
71
72	## Tasks (expanded 2026-06-03 at M4 start)
73
74	> **Authored against:** ADR-0002 (net baked-in), ADR-0003 (instances + #7/#8/#9 red-team),
75	> ADR-0005 (pairing + #10/#11/#12 red-team), ADR-0006 (multi-subnet), ADR-0007 (notif),
76	> ADR-0009 (endpoint ACL, node-tier), ADR-0004 §B/§D (broker ownership + update gate),
77	> KNOWN-HAZARDS 6.5 (`REQ-HAZARD-DIRECT-WRITE-PRECEDENCE`). Sub-tasks are the executable
78	> unit; each lands with its tag in the same commit (TRACEABILITY rule 1). Activate the
79	> listed reqs **at the sub-task that delivers them** (rule 5), not up front.
80
81	**Sequencing.** D0→D1 are the transport floor. D2 (pairing) and D3 (registry) both sit on
82	D1 and can interleave, but D3's cross-node resolution wants D2's trust material to be real,
83	so author D2 first. D4 (broker-owned endpoint) underpins D5/D7 (both ride broker QUIC
84	streams). D6 (Psyche sync) needs D3's per-subnet distribution. D8 (notif) reuses D3+D6
85	distribution. D9 is the closeout. **Each D-task is itself a milestone-sized body** — expect
86	a `/self-clear` + commune between most of them; commit atomically per sub-task.
87
88	### D0 — Scaffold `crates/spt-net`
89	- **D0a** New crate (sibling of spt-term/spt-runtime, above spt-msg). Workspace member; deps
90	  `spt-proto`/`spt-store`/`spt-msg`; `publish = false` (not public SDK, R-ARCH-2).
91	- **D0b** `net` cargo feature (default-on in `spt` ref binary, optional for lib consumers —
92	  ADR-0002). Empty `net`-gated module tree so a `--no-default-features` lib build stays clean.
93	- **Reqs:** REQ-NET-1 (`doc` stub only at D0; `impl`/`unit` land in D1). · **Acceptance:**
94	  `cargo check` + `--no-default-features` both compile; layering acyclic; `traceable-reqs check` green.
95
96	### D1 — Iroh endpoint + mDNS discovery
97	- **D1a** Iroh endpoint bound to the node's Ed25519 identity (`REQ-NODE-IDENTITY` already
98	  built in spt-proto — reuse, don't re-mint). Spike #2 shape (iroh 0.98).
99	- **D1b** n0 default relay + `self-host relay` config knob + plain-language disclosure string
100	  (ADR-0002 v1 relay stance).  · **REQ-NET-2**
101	- **D1c** mDNS LAN discovery: one node advertises, another finds it; connect on a `net` ALPN.
102	- **Reqs:** REQ-NET-1 (impl+unit), REQ-NET-2. · **Acceptance:** two local nodes discover + connect; E2E.
103
104	### D2 — TOTP-SPAKE2 pairing (security-critical)
105	- **D2a** TOTP seed = durable subnet secret; QR / `otpauth://` provisioning; SPAKE2 handshake
106	  using the current 6-digit code as the PAKE password (ADR-0005). Dedicated pre-trust pairing ALPN.
107	- **D2b** **Full pairing-transcript binding** — roles, both node pubkeys, subnet ID, seed
108	  epoch, TOTP time-step, confirmation MACs (red-team **#12**). Negative tests: MITM, replay,
109	  reflection, wrong-subnet, stale time-step.
110	- **D2c** **Seed epoch + rotation** — removing a node rotates the subnet seed so an old
111	  node/seed cannot rejoin; transcript binds the epoch (red-team **#10**). Trust-store delete
112	  alone is NOT revocation.
113	- **D2d** **Subnet-global rate limiting** — one active ceremony per subnet, shared attempt
114	  counter, exponential backoff; justify-or-drop the ±1 TOTP window (red-team **#11**).
115	- **D2e** Local trust store (authorized peer pubkeys) + TOFU + warn-on-key-change. · **REQ-PAIR-2**
116	- **D2f** Multi-subnet ceremony: discovery takes TOTP code + subnet-name; rendezvous token =
117	  `H(subnet-name ‖ TOTP-epoch)`; create-new names the subnet up front. · **REQ-PAIR-5, REQ-PAIR-4**
118	- **D2g** Elevation-gated per-subnet code fetch (Win UAC / Linux root or elevated agent
119	  endpoint; else authenticator app). `spt pair show-totp [--subnet|--create-new]`. · **REQ-PAIR-6, REQ-PAIR-3**
120	- **Reqs:** REQ-PAIR-1..6. · **New hazard reqs to register first (rule 3):** add
121	  `REQ-HAZARD-PAIR-TRANSCRIPT-BIND` (#12), `REQ-HAZARD-PAIR-SEED-ROTATION` (#10),
122	  `REQ-HAZARD-PAIR-RATE-LIMIT` (#11) to `traceable-reqs.toml` before satisfying.
123	- **Acceptance:** two machines pair via one TOTP code; trust material stored; all negative tests pass.
124	- **(REQ-PAIR-7 subnet icon = GUI-only consumer; defer to D9 unless trivial.)**
125
126	### D3 — Subnet registry + resolution policy
127	- **D3a** Per-subnet eventually-consistent registry `endpoint_id → [instances]` with
128	  status active/dormant/offline. · **REQ-INST-7**
129	- **D3b** **Registry lease/epoch via monotonic per-node epochs** (NOT wall-clock) so
130	  active/dormant transitions + partitions don't route to the wrong instance (red-team **#8**).
131	  Chaos-test stale entries + ambiguous active instance.
132	- **D3c** Resolution policy: local → most-recently-active → explicit `id@node`. Qualified
133	  addressing `[subnet:]id[@node]`; **ambiguity across visible subnets refuses + forces
134	  qualification** (no guessing). · **REQ-INST-10**
135	- **D3d** Multi-subnet membership (one node holds N trust-contexts/seeds; same-user only,
136	  cross-user seam shaped). Join-time bare-id collision check. · **REQ-INST-9**
137	- **D3e** Endpoint visibility per-`(endpoint,subnet)`: excluded = not advertised AND not
138	  routable; OR-of-defaults + per-`(E,S)` override; **visibility gates sync**. · **REQ-INST-12, REQ-INST-13**
139	- **D3f** `spt rename <id> <new_id>` rippled to all instances (registry, every perch,
140	  `a-<id>` context branch), collision-checked per target subnet, 6.5-reconciled. · **REQ-INST-11**
141	- **Reqs:** REQ-INST-7,9,10,11,12,13. **New hazard req (rule 3):** `REQ-HAZARD-REGISTRY-EPOCH-LEASE` (#8).
142	- **Acceptance:** an `endpoint_id` resolves to a live instance across nodes; ambiguous id refuses.
143
144	### D4 — QUIC-stream broker ownership (impl Spike #3 shape)
145	- **D4a** Move the Iroh endpoint + live QUIC streams into the **stable broker** per ADR-0004
146	  §B ownership table (Spike #3 validated the shape; build it for real). Broker owns the
147	  socket; brain attaches/detaches.
148	- **D4b** Stream survives a brain restart **gapless** (no endpoint interruption — composes
149	  with the M3b handoff substrate + REQ-UPD-3 guarantee).
150	- **D4c** `REQ-EP-4` PresenceChannel broker endpoint seam (day-one seam; minimal v1 surface).
151	- **Reqs:** ADR-0004 §B, REQ-EP-4 (seam). **Track (ADR-0002 SERIOUS #6):** start the
152	  fault-injection acceptance matrix (crash/hang Iroh, mDNS, registry, PTY broker independently).
153	- **Acceptance:** a live cross-node QUIC stream survives a brain restart gapless.
154
155	### D5 — Cross-node message + remote-drive of a running instance
156	- **D5a** WAN message delivery over the broker endpoint (the REQ-MSG path, now off-node). · **REQ-NET-1**
157	- **D5b** Remote-control mode (byte-stream terminal attach to an instance on another node;
158	  compute+files stay remote) — distinct from local operation. · **REQ-INST-8**
159	- **D5c** Off-node remote-drive **detection** + file transfer (handoff seam: `git pull` +
160	  fresh-with-preload resume; remote goes dormant, no teardown). · **REQ-REACH-1**
161	- **D5d** Endpoint access control (ADR-0009 node-tier): per-endpoint whitelist, origin-node
162	  gate, stateful-firewall (reply/outbound exempt), outer gate before grants. · **REQ-SEC-1**
163	- **Reqs:** REQ-NET-1, REQ-INST-8, REQ-REACH-1, REQ-SEC-1. · **Acceptance:** drive an agent
164	  on machine A from machine B; off-node access denied without a grant.
165
166	### D6 — Cross-node Psyche sync (P2P), retire gh-repo interim
167	- **D6a** P2P context replication replacing the interim `gh`-repo sync (ADR-0002 hub stays
168	  opt-in for `tracked/` only). Two-tier: live context → all instances; project context →
169	  same-project instances. · **REQ-NET-3, REQ-INST-2, REQ-INST-5**
170	- **D6b** **Distributed precedence/freshness merge** — precedence marker carries source +
171	  timestamp + **node identity**; freshness = newest-and-newer-than-mine; the distributed
172	  generalization of 6.5 (red-team **#7**). **Merge-model DECIDED (2026-06-03, best
173	  judgment):** per-node **monotonic epoch counters + a version vector** (one counter per
174	  node, persisted in branchstore). A write carries the author's vector; on receipt, if the
175	  incoming vector strictly dominates → accept; if dominated → drop (stale); if **concurrent**
176	  (neither dominates) → **surface as an explicit conflict**, never silently newest-wins.
177	  Rationale: text context snapshots written infrequently by ≤N nodes don't need CRDT
178	  machinery, but DO need correct causality + concurrent-write detection (#7's core); a
179	  version vector gives both cheaply and **unifies with D3b's epoch-lease (#8)** — one
180	  per-node monotonic epoch source serves registry leases *and* sync precedence. Wall-clock
181	  stays only as a human-facing tiebreaker hint inside a flagged conflict, never as the
182	  ordering authority. **Conflict-surface UX CONFIRMED with user (2026-06-03, ADR-0013):
183	  Psyche auto-reconcile** — both versions persist as tracked conflict artifacts (never
184	  lose either), the endpoint's own Psyche merges them in one bounded stdout-captured turn
185	  on the **active instance's node** (single reconciler; fallback lowest node id); merged
186	  write = `join(vA,vB)` + reconciler bump (dominates both parents, clears subnet-wide).
187	  **D7.5a (the bounded stdout-captured Psyche turn driver) is pulled forward into D6b**
188	  as its prerequisite; D7.5b/c stay at D7.5. Transport confirmed git-native: incremental
189	  bundles over broker streams, pull-based ref-scoped, shared DAG (no per-node divergent
190	  histories); BranchStore v1 = git-CLI-backed (REQ-STORE-1); hub mode stays a deferred
191	  opt-in seam.
192	- **D6c** Subnet-exclusive sync honored (hidden ⟹ not synced; per-endpoint membership list).
193	- **Reqs:** REQ-NET-3, REQ-INST-2, REQ-INST-5; `REQ-HAZARD-DIRECT-WRITE-PRECEDENCE` (extend
194	  coverage to multi-writer/node-id). · **Acceptance:** the mind follows the user across two
195	  machines (two-tier); concurrent-write conflict surfaced, not silently dropped.
196
197	### D7 — Update peer-propagation transport (into the M3c engine seam)
198	- **D7a** Plug the P2P transport into the **existing M3c update-engine seam** (`update::`
199	  front door); the subnet self-heals to latest. No re-architecture of the engine.
200	- **D7b** **Every propagated binary still passes the M3c signature/rollback gate per node**
201	  before any handoff — one compromised node must not poison the subnet (ADR-0004 §D). · **REQ-UPD-1**
202	- **Reqs:** REQ-UPD-1. · **Acceptance:** a signed update self-heals across the subnet; the
203	  Ed25519+rollback gate runs on every receiving node; a tampered propagated binary is rejected.
204
205	### D7.5 — Psyche outbound relay channel (prerequisite for D8) — ADR-0012
206	> Surfaced via grill-with-docs 2026-06-03. The Psyche is **sandboxed** (Read/Write/Edit
207	> only) — its **sole outbound is stdout** `<EVENT type="reply|notify">` intents, which the
208	> daemon parses + relays as its proxy. spt-core has **neither** a stdout-capturing
209	> live-Psyche driver **nor** a marker parser yet; the interim `spawn_session` `Stdio::null()`
210	> silently discards every Psyche reply/notify (KNOWN-HAZARDS 7.3).
211	- **D7.5a** Drive the live-Psyche turn as a **bounded, stdin-fed, stdout-CAPTURED** harness
212	  invocation (per-turn `--resume`, mirroring the sister) — **not** the detached
213	  `Stdio::null()` `spawn_session`. The captured stdout is the reply/notify channel.
214	  **Pulled forward into D6b** (ADR-0013: the conflict-reconcile turn needs it); D7.5
215	  consumes the driver built there.
216	- **D7.5b** Parse the Psyche's stdout `<EVENT type="reply">` / `<EVENT type="notify">`
217	  envelopes; extend the `spt-proto::event` type taxonomy (`+reply`, `+notify`).
218	- **D7.5c** Daemon relay + **sanitize** (the trust boundary): strip any Psyche-supplied
219	  `from=`/target, re-stamp `from=<self_id>`, **constrain routing** (`reply` → the
220	  `__REPLY_TO__` sender only; `notify` → the agent's own user/subnet only), validate body
221	  (4.1). Anti-spoof is a conformance invariant, not best-effort.
222	- **Reqs:** `REQ-HAZARD-PSYCHE-OUTBOUND-PROXY` (activate here), ADR-0012. · **Feeds:** D8
223	  (the `notify` relay is D8's Psyche-producer path; the `reply` half is psyche→agent
224	  messaging). · **Acceptance:** a Psyche turn emitting `<EVENT type="reply">` relays to the
225	  inbound sender; `<EVENT type="notify">` reaches the user; a Psyche-supplied `from=` is
226	  stripped (anti-spoof negative test); a null-stdout driver fails the guard.
227
228	### D8 — Subnet notification primitive
229	- **D8a** Notif as a first-class kind (user-directed, dismissable, resurfacing): per-subnet
230	  replicated spool (reuses D3 distribution), subnet-tagged, dismiss-state replicates
231	  subnet-wide. Two states seen/dismissed. · **REQ-NOTIF-1**
232	- **D8b** First-fire → most-recently-active endpoint in-subnet; resurface undismissed at
233	  reported boundaries (wake / api-clear / api-compact / new-session) gated by seen-per-endpoint
234	  + cross-endpoint suppression timeout (~1h). Scoped to visible subnets.
235	- **D8c** Refactor the M3c self-update prompt + consent escalation to be **notif producers**
236	  (one primitive, not three ad-hoc paths).
237	- **D8d** `spt notify` (agent-issued subnet notif) + `notif_command` manifest seam on harness
238	  + shell adapters; envelope `from` = issuer endpoint+node+subnet. · **REQ-NOTIF-2**
239	- **Reqs:** REQ-NOTIF-1, REQ-NOTIF-2. · **Acceptance:** a notif fires to the active endpoint,
240	  resurfaces at a boundary, dismiss replicates subnet-wide; `spt notify` reaches the user.
241
242	### D9 — Activation sweep + cross-node E2E + closeout
243	- **D9a** Dormancy resource budget (red-team **#9**): measure N dormant sessions on real
244	  adapters; define adapter warm/cold policy before locking warm default. · **REQ-INST-3, REQ-INST-4**
245	- **D9b** Two-host cross-node E2E on the real rig (gravity-linux ↔ a Windows host): pair →
246	  register → cross-node message → remote-drive → Psyche sync → update self-heal.
247	- **D9c** REQ-INST-14 (resource advertisement) + REQ-INST-15 (immutable home subnet + `spt
248	  fork`, ADR-0010) — register/activate if in V1-mid line; else document as forward seam.
249	- **D9d** Confirm every M4 req `required_stages` is satisfied; `traceable-reqs check` green;
250	  amend ROADMAP (M4 delivered) + CONTEXT glossary; CI matrix green (ubuntu+windows+traceability).
251	- **Acceptance:** the V1-mid scope line met; `check` green; CI green; cross-node proof demoed.
252
253	## Requirement → task coverage map
254
255	| Task | Activates |
256	|------|-----------|
257	| D0 | REQ-NET-1 (doc) |
258	| D1 | REQ-NET-1 (impl/unit), REQ-NET-2 |
259	| D2 | REQ-PAIR-1..6 + 3 new `REQ-HAZARD-PAIR-*` |
260	| D3 | REQ-INST-7,9,10,11,12,13 + `REQ-HAZARD-REGISTRY-EPOCH-LEASE` |
261	| D4 | REQ-EP-4 (seam), ADR-0004 §B |
262	| D5 | REQ-INST-8, REQ-REACH-1, REQ-SEC-1, REQ-NET-1 (WAN msg) |
263	| D6 | REQ-NET-3, REQ-INST-2, REQ-INST-5, `REQ-HAZARD-DIRECT-WRITE-PRECEDENCE` (extend) |
264	| D7 | REQ-UPD-1 |
265	| D7.5 | REQ-HAZARD-PSYCHE-OUTBOUND-PROXY (+ ADR-0012) |
266	| D8 | REQ-NOTIF-1, REQ-NOTIF-2 (notify-from-psyche path depends on D7.5) |
267	| D9 | REQ-INST-3,4 (+14,15 if in line); sweep |
268
269	**Deferred to M5 (out of M4, per Scope §Out):** REQ-EP-5 (shell instantiation), REQ-REACH-2
270	(remote command exec), PresenceChannel full impl, cross-user generalization of every per-subnet
271	key. Their seams are shaped in D4/D5/D8 but not built.
272
273	## Red-team obligations (binding — from the Stage-A review, must close in M4)
274
275	| Item | ADR | Lands in | Gate |
276	|------|-----|----------|------|
277	| #6 one-daemon failure domain | 0002 | D4 | fault-injection acceptance matrix |
278	| #7 wall-clock loses concurrent writes | 0003 | D6 | epoch/vector-clock merge, conflict surfaced |
279	| #8 ambiguous resolution under skew | 0003 | D3 | monotonic per-node epoch lease + chaos test |
280	| #9 dormant-warm resource cost | 0003 | D9 | measured budget + warm/cold policy |
281	| #10 trust-delete ≠ revocation | 0005 | D2c | seed epoch + rotation |
282	| #11 distributed PAKE guessing | 0005 | D2d | subnet-global rate limit, one ceremony |
283	| #12 transcript must bind | 0005 | D2b | full transcript spec + negative tests |
284
285	## Workspace change
286
287	Add `crates/spt-net` (the remaining sibling). Final layering (R-ARCH-1, acyclic):
288	`spt-proto → spt-store → spt-msg → {spt-net, spt-term, spt-runtime} → spt-live → spt-daemon → spt`.
289	`spt-net` is **not** public SDK (R-ARCH-2 stays proto/runtime/msg).
290

# M4 Plan — networking + instances

> **Status: COMPLETE (2026-06-04).** D0–D9 all delivered CI-green; the §D9b
> two-host proof ran on the real rig (CI run `26958175812`, HFENDULEAM ↔
> gravity-linux — `docs/TWO-HOST-RUNBOOK.md` evidence appendix); every M4 req
> satisfied at its `required_stages` or rule-5-deferred with rationale
> (`traceable-reqs check` green); ROADMAP §M4 records the delivery. Deferred
> seams annotated to M5/post-v1 in ROADMAP §M5 + §Out below.

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a/M3b/M3c-PLAN.md`.
> Task layer authored in full 2026-06-03 at M4 start (see §Tasks).
> Branch: `dev-freeform`. Authoritative architecture: **ADR-0002** (networking baked
> into core), **ADR-0003** (multi-instance node-anchored identity), **ADR-0005**
> (pairing), **ADR-0006/0007** (subnet membership + notification primitive), and
> `ROADMAP.md` §M4.

> **Upstream is done:** **M3 COMPLETE (2026-06-03)** — M3a (spt-term PTY) · M3b
> (broker/brain daemon) · M3c (signed self-update). The broker's resource-ownership
> table (ADR-0004 §B) and the update engine's transport seam are both built
> transport-agnostic precisely so M4 is an *integration*, not a re-architecture:
> M4 plugs the P2P transport into seams that already exist.

## Goal

Deliver the real cross-machine proof: `spt-net` (Iroh WAN + mDNS LAN + TOTP-SPAKE2
pairing + the eventually-consistent subnet registry) and the multi-instance model
(ADR-0003) so the same agent runs on each of a user's machines with a synced mind,
reachable and operable from anywhere — **cross-node message + drive an agent on
machine A from machine B**, plus update peer-propagation across the subnet.

## Scope

### In

- **`spt-net` crate** — the remaining sibling in the layering
  (`…→spt-msg→{spt-net, spt-term, spt-runtime}→…`): Iroh endpoint (WAN, the Spike #2
  shape) + mDNS (LAN discovery) + TOTP-SPAKE2 pairing (ADR-0005) (`R-NET-*`, `R-PAIR-*`).
- **Subnet registry** — eventually-consistent `endpoint_id → [instances]` + resolution
  policy (local → most-recently-active → `id@node`) (`REQ-INST-7`).
- **QUIC-stream broker ownership — the implementation.** Spike #3 validated the
  *shape* (a broker-owned Iroh endpoint + live QUIC stream survives a brain restart);
  M4 builds it for real into the broker's ADR-0004 §B ownership table.
- **Cross-node Psyche sync (P2P)** — replaces the interim gh-repo sync; the direct-write
  precedence marker + node id (KNOWN-HAZARDS 6.5).
- **Remote-drive of an already-running instance** — the network session-surface (the
  highest-value v1 slice; remote-drive detection + off-node file transfer).
- **Update peer-propagation transport (`REQ-UPD-1`)** — plug the P2P transport into the
  **M3c update-engine seam**: the subnet self-heals to latest, every propagated binary
  still passing the M3c signature/rollback gate before any handoff (one compromised node
  must not poison the subnet — ADR-0004 §D).

### Out

- **Shells, PresenceChannel, and the consent-gated capabilities** (remote-exec,
  instantiate-anywhere) — **M5**, landing into the v1 consent seam M3c-C2 already shaped.
- **Cross-user / per-(subnet, user)** generalization of the notif spool + dismissal — a
  documented forward seam, not v1.

## Requirements (activate per task when M4 starts)

`R-NET-*`, `R-PAIR-*`, `REQ-INST-7`, `REQ-UPD-1`, `REQ-EP-4` (PresenceChannel impl — or
M5), and the distributed precedence/freshness generalization of 6.5 — all currently
`required_stages = []`. Re-confirm the new-requirement assessment (TRACEABILITY rule 3)
at task-authoring time (pairing + subnet wire formats may surface `REQ-HAZARD-*`).

## Clean-room posture

Copy-verbatim only the sister's stable wire/registry **formats** that survive (ADR-0001);
clean-room the transport (the sister had no P2P). Reuse Spike #2/#3 Iroh learnings (iroh
0.98 API, the loopback QUIC-survival shape) — those were sandbox spikes, not in-repo crates.

## Tasks (expanded 2026-06-03 at M4 start)

> **Authored against:** ADR-0002 (net baked-in), ADR-0003 (instances + #7/#8/#9 red-team),
> ADR-0005 (pairing + #10/#11/#12 red-team), ADR-0006 (multi-subnet), ADR-0007 (notif),
> ADR-0009 (endpoint ACL, node-tier), ADR-0004 §B/§D (broker ownership + update gate),
> KNOWN-HAZARDS 6.5 (`REQ-HAZARD-DIRECT-WRITE-PRECEDENCE`). Sub-tasks are the executable
> unit; each lands with its tag in the same commit (TRACEABILITY rule 1). Activate the
> listed reqs **at the sub-task that delivers them** (rule 5), not up front.

**Sequencing.** D0→D1 are the transport floor. D2 (pairing) and D3 (registry) both sit on
D1 and can interleave, but D3's cross-node resolution wants D2's trust material to be real,
so author D2 first. D4 (broker-owned endpoint) underpins D5/D7 (both ride broker QUIC
streams). D6 (Psyche sync) needs D3's per-subnet distribution. D8 (notif) reuses D3+D6
distribution. D9 is the closeout. **Each D-task is itself a milestone-sized body** — expect
a `/self-clear` + commune between most of them; commit atomically per sub-task.

### D0 — Scaffold `crates/spt-net`
- **D0a** New crate (sibling of spt-term/spt-runtime, above spt-msg). Workspace member; deps
  `spt-proto`/`spt-store`/`spt-msg`; `publish = false` (not public SDK, R-ARCH-2).
- **D0b** `net` cargo feature (default-on in `spt` ref binary, optional for lib consumers —
  ADR-0002). Empty `net`-gated module tree so a `--no-default-features` lib build stays clean.
- **Reqs:** REQ-NET-1 (`doc` stub only at D0; `impl`/`unit` land in D1). · **Acceptance:**
  `cargo check` + `--no-default-features` both compile; layering acyclic; `traceable-reqs check` green.

### D1 — Iroh endpoint + mDNS discovery
- **D1a** Iroh endpoint bound to the node's Ed25519 identity (`REQ-NODE-IDENTITY` already
  built in spt-proto — reuse, don't re-mint). Spike #2 shape (iroh 0.98).
- **D1b** n0 default relay + `self-host relay` config knob + plain-language disclosure string
  (ADR-0002 v1 relay stance).  · **REQ-NET-2**
- **D1c** mDNS LAN discovery: one node advertises, another finds it; connect on a `net` ALPN.
- **Reqs:** REQ-NET-1 (impl+unit), REQ-NET-2. · **Acceptance:** two local nodes discover + connect; E2E.

### D2 — TOTP-SPAKE2 pairing (security-critical)
- **D2a** TOTP seed = durable subnet secret; QR / `otpauth://` provisioning; SPAKE2 handshake
  using the current 6-digit code as the PAKE password (ADR-0005). Dedicated pre-trust pairing ALPN.
- **D2b** **Full pairing-transcript binding** — roles, both node pubkeys, subnet ID, seed
  epoch, TOTP time-step, confirmation MACs (red-team **#12**). Negative tests: MITM, replay,
  reflection, wrong-subnet, stale time-step.
- **D2c** **Seed epoch + rotation** — removing a node rotates the subnet seed so an old
  node/seed cannot rejoin; transcript binds the epoch (red-team **#10**). Trust-store delete
  alone is NOT revocation.
- **D2d** **Subnet-global rate limiting** — one active ceremony per subnet, shared attempt
  counter, exponential backoff; justify-or-drop the ±1 TOTP window (red-team **#11**).
- **D2e** Local trust store (authorized peer pubkeys) + TOFU + warn-on-key-change. · **REQ-PAIR-2**
- **D2f** Multi-subnet ceremony: discovery takes TOTP code + subnet-name; rendezvous token =
  `H(subnet-name ‖ TOTP-epoch)`; create-new names the subnet up front. · **REQ-PAIR-5, REQ-PAIR-4**
- **D2g** Elevation-gated per-subnet code fetch (Win UAC / Linux root or elevated agent
  endpoint; else authenticator app). `spt pair show-totp [--subnet|--create-new]`. · **REQ-PAIR-6, REQ-PAIR-3**
- **Reqs:** REQ-PAIR-1..6. · **New hazard reqs to register first (rule 3):** add
  `REQ-HAZARD-PAIR-TRANSCRIPT-BIND` (#12), `REQ-HAZARD-PAIR-SEED-ROTATION` (#10),
  `REQ-HAZARD-PAIR-RATE-LIMIT` (#11) to `traceable-reqs.toml` before satisfying.
- **Acceptance:** two machines pair via one TOTP code; trust material stored; all negative tests pass.
- **(REQ-PAIR-7 subnet icon = GUI-only consumer; defer to D9 unless trivial.)**

### D3 — Subnet registry + resolution policy
- **D3a** Per-subnet eventually-consistent registry `endpoint_id → [instances]` with
  status active/dormant/offline. · **REQ-INST-7**
- **D3b** **Registry lease/epoch via monotonic per-node epochs** (NOT wall-clock) so
  active/dormant transitions + partitions don't route to the wrong instance (red-team **#8**).
  Chaos-test stale entries + ambiguous active instance.
- **D3c** Resolution policy: local → most-recently-active → explicit `id@node`. Qualified
  addressing `[subnet:]id[@node]`; **ambiguity across visible subnets refuses + forces
  qualification** (no guessing). · **REQ-INST-10**
- **D3d** Multi-subnet membership (one node holds N trust-contexts/seeds; same-user only,
  cross-user seam shaped). Join-time bare-id collision check. · **REQ-INST-9**
- **D3e** Endpoint visibility per-`(endpoint,subnet)`: excluded = not advertised AND not
  routable; OR-of-defaults + per-`(E,S)` override; **visibility gates sync**. · **REQ-INST-12, REQ-INST-13**
- **D3f** `spt rename <id> <new_id>` rippled to all instances (registry, every perch,
  `a-<id>` context branch), collision-checked per target subnet, 6.5-reconciled. · **REQ-INST-11**
- **Reqs:** REQ-INST-7,9,10,11,12,13. **New hazard req (rule 3):** `REQ-HAZARD-REGISTRY-EPOCH-LEASE` (#8).
- **Acceptance:** an `endpoint_id` resolves to a live instance across nodes; ambiguous id refuses.

### D4 — QUIC-stream broker ownership (impl Spike #3 shape)
- **D4a** Move the Iroh endpoint + live QUIC streams into the **stable broker** per ADR-0004
  §B ownership table (Spike #3 validated the shape; build it for real). Broker owns the
  socket; brain attaches/detaches.
- **D4b** Stream survives a brain restart **gapless** (no endpoint interruption — composes
  with the M3b handoff substrate + REQ-UPD-3 guarantee).
- **D4c** `REQ-EP-4` PresenceChannel broker endpoint seam (day-one seam; minimal v1 surface).
- **Reqs:** ADR-0004 §B, REQ-EP-4 (seam). **Track (ADR-0002 SERIOUS #6):** start the
  fault-injection acceptance matrix (crash/hang Iroh, mDNS, registry, PTY broker independently).
- **Acceptance:** a live cross-node QUIC stream survives a brain restart gapless.

### D5 — Cross-node message + remote-drive of a running instance
- **D5a** WAN message delivery over the broker endpoint (the REQ-MSG path, now off-node). · **REQ-NET-1**
- **D5b** Remote-control mode (byte-stream terminal attach to an instance on another node;
  compute+files stay remote) — distinct from local operation. · **REQ-INST-8**
- **D5c** Off-node remote-drive **detection** + file transfer (handoff seam: `git pull` +
  fresh-with-preload resume; remote goes dormant, no teardown). · **REQ-REACH-1**
- **D5d** Endpoint access control (ADR-0009 node-tier): per-endpoint whitelist, origin-node
  gate, stateful-firewall (reply/outbound exempt), outer gate before grants. · **REQ-SEC-1**
- **Reqs:** REQ-NET-1, REQ-INST-8, REQ-REACH-1, REQ-SEC-1. · **Acceptance:** drive an agent
  on machine A from machine B; off-node access denied without a grant.

### D6 — Cross-node Psyche sync (P2P), retire gh-repo interim
- **D6a** P2P context replication replacing the interim `gh`-repo sync (ADR-0002 hub stays
  opt-in for `tracked/` only). Two-tier: live context → all instances; project context →
  same-project instances. · **REQ-NET-3, REQ-INST-2, REQ-INST-5**
- **D6b** **Distributed precedence/freshness merge** — precedence marker carries source +
  timestamp + **node identity**; freshness = newest-and-newer-than-mine; the distributed
  generalization of 6.5 (red-team **#7**). **Merge-model DECIDED (2026-06-03, best
  judgment):** per-node **monotonic epoch counters + a version vector** (one counter per
  node, persisted in branchstore). A write carries the author's vector; on receipt, if the
  incoming vector strictly dominates → accept; if dominated → drop (stale); if **concurrent**
  (neither dominates) → **surface as an explicit conflict**, never silently newest-wins.
  Rationale: text context snapshots written infrequently by ≤N nodes don't need CRDT
  machinery, but DO need correct causality + concurrent-write detection (#7's core); a
  version vector gives both cheaply and **unifies with D3b's epoch-lease (#8)** — one
  per-node monotonic epoch source serves registry leases *and* sync precedence. Wall-clock
  stays only as a human-facing tiebreaker hint inside a flagged conflict, never as the
  ordering authority. **Conflict-surface UX CONFIRMED with user (2026-06-03, ADR-0013):
  Psyche auto-reconcile** — both versions persist as tracked conflict artifacts (never
  lose either), the endpoint's own Psyche merges them in one bounded stdout-captured turn
  on the **active instance's node** (single reconciler; fallback lowest node id); merged
  write = `join(vA,vB)` + reconciler bump (dominates both parents, clears subnet-wide).
  **D7.5a (the bounded stdout-captured Psyche turn driver) is pulled forward into D6b**
  as its prerequisite; D7.5b/c stay at D7.5. Transport confirmed git-native: incremental
  bundles over broker streams, pull-based ref-scoped, shared DAG (no per-node divergent
  histories); BranchStore v1 = git-CLI-backed (REQ-STORE-1); hub mode stays a deferred
  opt-in seam.
- **D6c** Subnet-exclusive sync honored (hidden ⟹ not synced; per-endpoint membership list).
- **Reqs:** REQ-NET-3, REQ-INST-2, REQ-INST-5; `REQ-HAZARD-DIRECT-WRITE-PRECEDENCE` (extend
  coverage to multi-writer/node-id). · **Acceptance:** the mind follows the user across two
  machines (two-tier); concurrent-write conflict surfaced, not silently dropped.

### D7 — Update peer-propagation transport (into the M3c engine seam)
- **D7a** Plug the P2P transport into the **existing M3c update-engine seam** (`update::`
  front door); the subnet self-heals to latest. No re-architecture of the engine.
- **D7b** **Every propagated binary still passes the M3c signature/rollback gate per node**
  before any handoff — one compromised node must not poison the subnet (ADR-0004 §D). · **REQ-UPD-1**
- **Reqs:** REQ-UPD-1. · **Acceptance:** a signed update self-heals across the subnet; the
  Ed25519+rollback gate runs on every receiving node; a tampered propagated binary is rejected.

### D7.5 — Psyche outbound relay channel (prerequisite for D8) — ADR-0012
> Surfaced via grill-with-docs 2026-06-03. The Psyche is **sandboxed** (Read/Write/Edit
> only) — its **sole outbound is stdout** `<EVENT type="reply|notify">` intents, which the
> daemon parses + relays as its proxy. spt-core has **neither** a stdout-capturing
> live-Psyche driver **nor** a marker parser yet; the interim `spawn_session` `Stdio::null()`
> silently discards every Psyche reply/notify (KNOWN-HAZARDS 7.3).
- **D7.5a** Drive the live-Psyche turn as a **bounded, stdin-fed, stdout-CAPTURED** harness
  invocation (per-turn `--resume`, mirroring the sister) — **not** the detached
  `Stdio::null()` `spawn_session`. The captured stdout is the reply/notify channel.
  **Pulled forward into D6b** (ADR-0013: the conflict-reconcile turn needs it); D7.5
  consumes the driver built there.
- **D7.5b** Parse the Psyche's stdout `<EVENT type="reply">` / `<EVENT type="notify">`
  envelopes; extend the `spt-proto::event` type taxonomy (`+reply`, `+notify`).
- **D7.5c** Daemon relay + **sanitize** (the trust boundary): strip any Psyche-supplied
  `from=`/target, re-stamp `from=<self_id>`, **constrain routing** (`reply` → the
  `__REPLY_TO__` sender only; `notify` → the agent's own user/subnet only), validate body
  (4.1). Anti-spoof is a conformance invariant, not best-effort.
- **Reqs:** `REQ-HAZARD-PSYCHE-OUTBOUND-PROXY` (activate here), ADR-0012. · **Feeds:** D8
  (the `notify` relay is D8's Psyche-producer path; the `reply` half is psyche→agent
  messaging). · **Acceptance:** a Psyche turn emitting `<EVENT type="reply">` relays to the
  inbound sender; `<EVENT type="notify">` reaches the user; a Psyche-supplied `from=` is
  stripped (anti-spoof negative test); a null-stdout driver fails the guard.

### D8 — Subnet notification primitive
- **D8a** Notif as a first-class kind (user-directed, dismissable, resurfacing): per-subnet
  replicated spool (reuses D3 distribution), subnet-tagged, dismiss-state replicates
  subnet-wide. Two states seen/dismissed. · **REQ-NOTIF-1**
- **D8b** First-fire → most-recently-active endpoint in-subnet; resurface undismissed at
  reported boundaries (wake / api-clear / api-compact / new-session) gated by seen-per-endpoint
  + cross-endpoint suppression timeout (~1h). Scoped to visible subnets.
- **D8c** Refactor the M3c self-update prompt + consent escalation to be **notif producers**
  (one primitive, not three ad-hoc paths).
- **D8d** `spt notify` (agent-issued subnet notif) + `notif_command` manifest seam on harness
  + shell adapters; envelope `from` = issuer endpoint+node+subnet. · **REQ-NOTIF-2**
- **Reqs:** REQ-NOTIF-1, REQ-NOTIF-2. · **Acceptance:** a notif fires to the active endpoint,
  resurfaces at a boundary, dismiss replicates subnet-wide; `spt notify` reaches the user.

### D9 — Activation sweep + cross-node E2E + closeout
- **D9a** Dormancy resource budget (red-team **#9**): measure N dormant sessions on real
  adapters; define adapter warm/cold policy before locking warm default. · **REQ-INST-3, REQ-INST-4**
- **D9b** Two-host cross-node E2E on the real rig (gravity-linux ↔ a Windows host): pair →
  register → cross-node message → remote-drive → Psyche sync → update self-heal.
- **D9c** REQ-INST-14 (resource advertisement) + REQ-INST-15 (immutable home subnet + `spt
  fork`, ADR-0010) — register/activate if in V1-mid line; else document as forward seam.
- **D9d** Confirm every M4 req `required_stages` is satisfied; `traceable-reqs check` green;
  amend ROADMAP (M4 delivered) + CONTEXT glossary; CI matrix green (ubuntu+windows+traceability).
- **Acceptance:** the V1-mid scope line met; `check` green; CI green; cross-node proof demoed.

## Requirement → task coverage map

| Task | Activates |
|------|-----------|
| D0 | REQ-NET-1 (doc) |
| D1 | REQ-NET-1 (impl/unit), REQ-NET-2 |
| D2 | REQ-PAIR-1..6 + 3 new `REQ-HAZARD-PAIR-*` |
| D3 | REQ-INST-7,9,10,11,12,13 + `REQ-HAZARD-REGISTRY-EPOCH-LEASE` |
| D4 | REQ-EP-4 (seam), ADR-0004 §B |
| D5 | REQ-INST-8, REQ-REACH-1, REQ-SEC-1, REQ-NET-1 (WAN msg) |
| D6 | REQ-NET-3, REQ-INST-2, REQ-INST-5, `REQ-HAZARD-DIRECT-WRITE-PRECEDENCE` (extend) |
| D7 | REQ-UPD-1 |
| D7.5 | REQ-HAZARD-PSYCHE-OUTBOUND-PROXY (+ ADR-0012) |
| D8 | REQ-NOTIF-1, REQ-NOTIF-2 (notify-from-psyche path depends on D7.5) |
| D9 | REQ-INST-3,4 (+14,15 if in line); sweep |

**Deferred to M5 (out of M4, per Scope §Out):** REQ-EP-5 (shell instantiation), REQ-REACH-2
(remote command exec), PresenceChannel full impl, cross-user generalization of every per-subnet
key. Their seams are shaped in D4/D5/D8 but not built.

## Red-team obligations (binding — from the Stage-A review, must close in M4)

| Item | ADR | Lands in | Gate |
|------|-----|----------|------|
| #6 one-daemon failure domain | 0002 | D4 | fault-injection acceptance matrix |
| #7 wall-clock loses concurrent writes | 0003 | D6 | epoch/vector-clock merge, conflict surfaced |
| #8 ambiguous resolution under skew | 0003 | D3 | monotonic per-node epoch lease + chaos test |
| #9 dormant-warm resource cost | 0003 | D9 | measured budget + warm/cold policy |
| #10 trust-delete ≠ revocation | 0005 | D2c | seed epoch + rotation |
| #11 distributed PAKE guessing | 0005 | D2d | subnet-global rate limit, one ceremony |
| #12 transcript must bind | 0005 | D2b | full transcript spec + negative tests |

## Workspace change

Add `crates/spt-net` (the remaining sibling). Final layering (R-ARCH-1, acyclic):
`spt-proto → spt-store → spt-msg → {spt-net, spt-term, spt-runtime} → spt-live → spt-daemon → spt`.
`spt-net` is **not** public SDK (R-ARCH-2 stays proto/runtime/msg).


152:id = "REQ-EP-4"
153-title = "PresenceChannel broker endpoint (seam day-one)"
154-[Omitted long context line]
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 = []  # rule 5: M5 — shell instantiation rides the per-agent-runtime/daemon-hosted-shells work; seams shaped in D4/D5/D8 (M4-PLAN §Out)
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 at M5 when instantiate-anywhere makes the split user-facing (instance minting); 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-[Omitted long context line]
169-[[requirements]]
170:id = "REQ-INST-3"
171-title = "Dormant (warm) / suspended (cold) resting states"
172-[Omitted long context line]
173-[[requirements]]
174:id = "REQ-INST-4"
175-title = "active to dormant/suspended fires a transition echo commune"
176-[Omitted long context line]
177-[[requirements]]
178-id = "REQ-INST-5"
179-title = "Two-tier context sync (live to all, project to same-project)"
180-[Omitted long context line]
181-[[requirements]]
182:id = "REQ-INST-6"
183-title = "Deferred messages not delivered to dormant/suspended instances"
184-required_stages = []  # rule 5: M5. The precondition shipped M4-D9-2 (the explicit resting-state machine + durable info.json rest_state record — the gate's input is now a readable fact); the gate itself (deferred-row hold while resting, release on the wake edge) rides the M5 delivery work. Seam documented in spt-daemon::resting module docs
185-[[requirements]]
186-id = "REQ-INST-7"
187-title = "Subnet registry + bare-id resolution policy"
188-[Omitted long context line]
189-[[requirements]]
190-id = "REQ-INST-8"
--
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]]
--
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]]
--
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 = []  # rule 5: M5 — adapter registration lifecycle lands with the adapter-hosting work (manifest seams already shipped, REQ-SEAM-*)
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"
--
416:id = "REQ-HAZARD-STALE-INDEX-LOCK"
417-title = "Sweep stale lockfiles on daemon boot (1.3)"
418-required_stages = []  # rule 5: binds when the daemon owns git-index-touching sweeps at boot — the BranchStore git runner (gitrun.rs) is bounded per ADR-0013 and no boot sweep exists yet (M5 daemon-hosted shells/runtimes)
419-[[requirements]]
420-id = "REQ-HAZARD-DEFERRED-DRAIN"
421-title = "Deferred spool rows excluded from the event-stream drain (1.4)"
422-required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
423-[[requirements]]
424-id = "REQ-HAZARD-WORKER-PATH"
--
432:id = "REQ-HAZARD-STDIN-SESSION-ID"
433-title = "Stdin session_id precedence over env (2.2)"
434-required_stages = []  # rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such adapter path exists in spt-core yet (M5 adapter work)
435-[[requirements]]
436-id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
437-title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
438-required_stages = ["impl", "unit"]
439-[[requirements]]
440-id = "REQ-HAZARD-GEN-START-NOW"


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 = []  # rule 5: M5 — shell instantiation rides the per-agent-runtime/daemon-hosted-shells work; seams shaped in D4/D5/D8 (M4-PLAN §Out)
159

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 = []  # rule 5: M5 — shell instantiation rides the per-agent-runtime/daemon-hosted-shells work; seams shaped in D4/D5/D8 (M4-PLAN §Out)


252:### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`
253-[Omitted long context line]
254-- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.
255-- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, "all Psyche/pulse loops" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.
256-- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).
257-
258-### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`
--
293:| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |
294-| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |


AskUserQuestion

Question: PresenceChannel depth in M5? ROADMAP §M5 lists it; DEFERRED.md says impl deferred past v1 (trigger: 'once Shells exist + registry stable' — met by M5 shells). M4-D4c shipped the broker PresenceLog seam.

Choices:
Presence gossip (last_active_node/endpoint/ts subnet-wide) + most-recently-active routing made first-class (notifs/consent already use a precursor). Channel styles (dispatch/bind/thread) stay deferred.
Broker endpoint with dispatch/bind/thread styles + durable shell-agnostic thread + presence gossip. Largest scope; needs shells live first.
Keep DEFERRED.md stance — no new presence work in M5 beyond what notif/consent routing already does. ROADMAP §M5 wording amended.

Question: Consent-gated capabilities depth in M5? ROADMAP says remote-exec + instantiate-anywhere land into the v1 consent seam; PRD/DEFERRED list both as post-v1 (but revisit-triggers are met — remote-drive shipped M4).

Choices:
Build the real consent framework (grant store + interactive escalation, CONTEXT §Consent) and land instantiate-anywhere into it. Remote-exec stays deferred (workarounds exist; highest-risk). Matches REQ-REACH-2 rationale.
Framework + instantiate-anywhere + remote command execution. Full ROADMAP §M5 reading; updates the v1 scope line in PRD/DEFERRED.
Grant store shape + consent-prompt delivery only; both capabilities stay deferred. Minimal reading.

Question: Shell proof depth for M5? DEFERRED.md keeps concrete shells (GameRobot) out; clean-room rule says mock/test adapters in-tree.

Choices:
Full shell machinery (spawn/relink/teardown, 3 channels, wake-watcher, approval/cap gates, aliasing) proven via a generic mock shell adapter, including a cross-node owner↔shell link on the real rig ([twohost] CI).
Same machinery, but cross-node shell link deferred — proof is local-node only. Smaller; cross-node link rides instantiate-anywhere later.
Machinery + mock, plus one minimal real shell (e.g. OS-notification shell) as a dogfood proof. Breaks the concrete-shells deferral deliberately.

Question: Process: grill session before authoring M5-PLAN.md (M4 precedent), or author directly from these answers?

Choices:
These 3 answers + carried seams are enough; author M5-PLAN.md now, surface further forks as they arise per-task.
Full grill session against CONTEXT/ADRs to stress-test M5 scope and update docs inline before plan authoring (M4 did this).


Your questions have been answered: "PresenceChannel depth in M5? ROADMAP §M5 lists it; DEFERRED.md says impl deferred past v1 (trigger: 'once Shells exist + registry stable' — met by M5 shells). M4-D4c shipped the broker PresenceLog seam."="Presence resolution only", "Consent-gated capabilities depth in M5? ROADMAP says remote-exec + instantiate-anywhere land into the v1 consent seam; PRD/DEFERRED list both as post-v1 (but revisit-triggers are met — remote-drive shipped M4)."="Framework seam only", "Shell proof depth for M5? DEFERRED.md keeps concrete shells (GameRobot) out; clean-room rule says mock/test adapters in-tree."="Mock + one real shell", "Process: grill session before authoring M5-PLAN.md (M4 precedent), or author directly from these answers?"="Author plan directly". You can now continue with these answers in mind.

Question: PresenceChannel depth in M5? ROADMAP §M5 lists it; DEFERRED.md says impl deferred past v1 (trigger: 'once Shells exist + registry stable' — met by M5 shells). M4-D4c shipped the broker PresenceLog seam.

Choices:
Presence gossip (last_active_node/endpoint/ts subnet-wide) + most-recently-active routing made first-class (notifs/consent already use a precursor). Channel styles (dispatch/bind/thread) stay deferred.
Broker endpoint with dispatch/bind/thread styles + durable shell-agnostic thread + presence gossip. Largest scope; needs shells live first.
Keep DEFERRED.md stance — no new presence work in M5 beyond what notif/consent routing already does. ROADMAP §M5 wording amended.

Question: Consent-gated capabilities depth in M5? ROADMAP says remote-exec + instantiate-anywhere land into the v1 consent seam; PRD/DEFERRED list both as post-v1 (but revisit-triggers are met — remote-drive shipped M4).

Choices:
Build the real consent framework (grant store + interactive escalation, CONTEXT §Consent) and land instantiate-anywhere into it. Remote-exec stays deferred (workarounds exist; highest-risk). Matches REQ-REACH-2 rationale.
Framework + instantiate-anywhere + remote command execution. Full ROADMAP §M5 reading; updates the v1 scope line in PRD/DEFERRED.
Grant store shape + consent-prompt delivery only; both capabilities stay deferred. Minimal reading.

Question: Shell proof depth for M5? DEFERRED.md keeps concrete shells (GameRobot) out; clean-room rule says mock/test adapters in-tree.

Choices:
Full shell machinery (spawn/relink/teardown, 3 channels, wake-watcher, approval/cap gates, aliasing) proven via a generic mock shell adapter, including a cross-node owner↔shell link on the real rig ([twohost] CI).
Same machinery, but cross-node shell link deferred — proof is local-node only. Smaller; cross-node link rides instantiate-anywhere later.
Machinery + mock, plus one minimal real shell (e.g. OS-notification shell) as a dogfood proof. Breaks the concrete-shells deferral deliberately.

Question: Process: grill session before authoring M5-PLAN.md (M4 precedent), or author directly from these answers?

Choices:
These 3 answers + carried seams are enough; author M5-PLAN.md now, surface further forks as they arise per-task.
Full grill session against CONTEXT/ADRs to stress-test M5 scope and update docs inline before plan authoring (M4 did this).


98:id = "REQ-SEAM-CAPABILITY"
99-title = "Hostable endpoint-types capability declaration"
100-required_stages = ["impl", "unit"]
101-[[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]]
--
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]]
--
562:id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
563-title = "Per-agent pulse\psyche\echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
564-[Omitted long context line]
565-# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10\#11\#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---


M4-D2-WIRE-PLAN.md:42:- **Q1 — rendezvous vs iroh dial.** CONTEXT:481 says the relay routes pre-trust pairing by a
M4-D2-WIRE-PLAN.md:62:**Q1 — rendezvous: DEFER to D9.** The driver dials by `EndpointAddr` (loopback,
crates\spt-net\src\net\pairing\rendezvous.rs:29://! D9 (the Q1 rendezvous deferral). The token is a pure function of public-ish inputs and
M4-D9-PLAN.md:127:| Relay rendezvous routing (D2f Q1) | D2f | **M5** (document, not build — no consumer until shells) |
M4-D9-PLAN.md:296:- Relay rendezvous routing (D2f Q1) — M5, no consumer until shells.
ROADMAP.md:39:- **M5 — Shells, presence, deferred capabilities** (NEXT). Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam. Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, remote fork arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.
traceable-reqs.toml:285:[Omitted long matching line]


M3c-PLAN.md:112:2. **`apply_brain_only()`** — the REQ-UPD-3 swap. Guards that the class **is**
M3c-PLAN.md:122:   axis inputs; `apply_brain_only` refuses a non-brain-only plan.
M4-D9-PLAN.md:5:comment — 19 inactive reqs annotated; `apply_brain_only` no-production-caller
M4-D7-PLAN.md:29:  transport that *feeds* it; apply stays C0's `apply_brain_only` (proven E2E at
ROADMAP.md:39:- **M5 — Shells, presence, deferred capabilities** (NEXT). Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam. Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, remote fork arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.
traceable-reqs.toml: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
crates\spt-daemon\tests\brain_swap.rs:6://! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)
crates\spt-daemon\tests\brain_swap.rs:32:use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};
crates\spt-daemon\tests\brain_swap.rs:114:    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))
crates\spt-daemon\src\lib.rs:45://! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
crates\spt-daemon\src\lib.rs:57://! every check, so an unverified binary can never reach [`apply_brain_only`].
crates\spt-daemon\src\lib.rs:193:    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
crates\spt-daemon\src\update.rs:22://! ([`apply_brain_only`]). The apply *triggers a handoff* over the existing M3b
crates\spt-daemon\src\update.rs:147:/// is produced** — there is nothing for [`apply_brain_only`] to act on, so an
crates\spt-daemon\src\update.rs:182:                write!(f, "apply_brain_only refused a {c:?} plan (would risk an endpoint)")

[Showing results with pagination = limit: 15]


110	their own, not only under test harnesses); the dormant/suspended resting model built and
111	**budgeted on real adapters** (red-team #9 closed with numbers); resource advertisement
112	+ immutable-home/fork shipped (REQ-INST-3, -4, -14, -15); the two-host proof demoed on
113	the rig (HFENDULEAM ↔ gravity-linux) with `[int->REQ-*]` evidence; the gh-repo interim
114	retired; every M4 req's `required_stages` satisfied; CI matrix green.
115
116	## The deferred-to-D9 ledger (all land here)
117
118	| Item | Deferred at | Lands in |
119	|------|-------------|----------|
120	| Production trigger loops (sync notify/anti-entropy, update check, notif feed pump, post-sync reconcile election) | D5/D6/D7/D8 | D9-1 |
121	| Production `VerifyPolicy` assembly (trusted-key store, channel pin, running version) | D7 | D9-1 |
122	| `spt send` WAN leg CLI wiring | D5 | D9-1 |
123	| Wake-edge (state→active) resurface hook | D8 | D9-2 |
124	| Dormancy resource budget + warm/cold policy (red-team #9) | ADR-0003 | D9-2/D9-3 |
125	| REQ-NOTIF / sync / update / msg two-host int legs | D5–D8 | D9-6 |
126	| gh-repo interim sync retirement | D6 | D9-6 |
127	| Relay rendezvous routing (D2f Q1) | D2f | **M5** (document, not build — no consumer until shells) |
128	| REQ-PAIR-7 subnet icon | D2 | **GUI milestone** (user 2026-06-04) |
129	| Adapter-payload propagation; `p-` tier per-file visibility filtering | D6/D7 | **post-v1** (unchanged) |
130
131	## Decisions (locked)
132
133	- **Two-host = env-gated tests + runbook.** Int tests gate on `SPT_TWO_HOST=1`
134	  (+ peer address/role env), skip silently otherwise — CI never runs them; the rig run
135	  is manual, scripted by `docs/TWO-HOST-RUNBOOK.md`, evidence tags live **on the gated
136	  tests** (real runs on real hosts). CI keeps the loopback int coverage it already has.
137	- **Trigger loops are thin calls over existing drivers** (`select_refs`/`request_sync`,
138	  `request_update`/`serve_update`, D8-2 notif emit/apply, `reconcile_after_sync`) —
139	  D9 wires *when*, the drivers already own *how*. Same posture as every D5–D8 deferral
140	  note. Per-loop period/enable knobs in `DaemonConfig`; loops named, observable, and
141	  stoppable like `run_pulse_loop`.
142	- **Pump loops journal with `None`** (gotcha #1) and snapshot stream tables before
143	  opening requester streams (gotcha #9) — inherited test discipline, now production
144	  discipline.

their own, not only under test harnesses); the dormant/suspended resting model built and
**budgeted on real adapters** (red-team #9 closed with numbers); resource advertisement
+ immutable-home/fork shipped (REQ-INST-3, -4, -14, -15); the two-host proof demoed on
the rig (HFENDULEAM ↔ gravity-linux) with `[int->REQ-*]` evidence; the gh-repo interim
retired; every M4 req's `required_stages` satisfied; CI matrix green.

## The deferred-to-D9 ledger (all land here)

| Item | Deferred at | Lands in |
|------|-------------|----------|
| Production trigger loops (sync notify/anti-entropy, update check, notif feed pump, post-sync reconcile election) | D5/D6/D7/D8 | D9-1 |
| Production `VerifyPolicy` assembly (trusted-key store, channel pin, running version) | D7 | D9-1 |
| `spt send` WAN leg CLI wiring | D5 | D9-1 |
| Wake-edge (state→active) resurface hook | D8 | D9-2 |
| Dormancy resource budget + warm/cold policy (red-team #9) | ADR-0003 | D9-2/D9-3 |
| REQ-NOTIF / sync / update / msg two-host int legs | D5–D8 | D9-6 |
| gh-repo interim sync retirement | D6 | D9-6 |
| Relay rendezvous routing (D2f Q1) | D2f | **M5** (document, not build — no consumer until shells) |
| REQ-PAIR-7 subnet icon | D2 | **GUI milestone** (user 2026-06-04) |
| Adapter-payload propagation; `p-` tier per-file visibility filtering | D6/D7 | **post-v1** (unchanged) |

## Decisions (locked)

- **Two-host = env-gated tests + runbook.** Int tests gate on `SPT_TWO_HOST=1`
  (+ peer address/role env), skip silently otherwise — CI never runs them; the rig run
  is manual, scripted by `docs/TWO-HOST-RUNBOOK.md`, evidence tags live **on the gated
  tests** (real runs on real hosts). CI keeps the loopback int coverage it already has.
- **Trigger loops are thin calls over existing drivers** (`select_refs`/`request_sync`,
  `request_update`/`serve_update`, D8-2 notif emit/apply, `reconcile_after_sync`) —
  D9 wires *when*, the drivers already own *how*. Same posture as every D5–D8 deferral
  note. Per-loop period/enable knobs in `DaemonConfig`; loops named, observable, and
  stoppable like `run_pulse_loop`.
- **Pump loops journal with `None`** (gotcha #1) and snapshot stream tables before
  opening requester streams (gotcha #9) — inherited test discipline, now production
  discipline.


285	     (ubuntu + windows + traceability) verified FINAL via `gh run list` (gotcha #7).
286
287	## Out of scope (unchanged by D9)
288
289	- REQ-INST-6 deferred-message instance-state gate — M5 (seam documented in D9-2).
290	- REQ-PAIR-7 subnet icon — GUI milestone (user 2026-06-04).
291	- Cross-runner CI two-host job — M5 seam (runbook is the v1 proof).
292	- Remote fork arm (cross-node `spt fork`) — M5, lands with instantiate-anywhere's
293	  consent UX.
294	- REQ-REACH-2, REQ-EP-5, PresenceChannel full impl, cross-user generalizations —
295	  M5 / post-v1 per M4-PLAN §Out.
296	- Relay rendezvous routing (D2f Q1) — M5, no consumer until shells.
297

     (ubuntu + windows + traceability) verified FINAL via `gh run list` (gotcha #7).

## Out of scope (unchanged by D9)

- REQ-INST-6 deferred-message instance-state gate — M5 (seam documented in D9-2).
- REQ-PAIR-7 subnet icon — GUI milestone (user 2026-06-04).
- Cross-runner CI two-host job — M5 seam (runbook is the v1 proof).
- Remote fork arm (cross-node `spt fork`) — M5, lands with instantiate-anywhere's
  consent UX.
- REQ-REACH-2, REQ-EP-5, PresenceChannel full impl, cross-user generalizations —
  M5 / post-v1 per M4-PLAN §Out.
- Relay rendezvous routing (D2f Q1) — M5, no consumer until shells.


Remote fork arm rides instantiate-anywhere → defers with it per "framework seam only" decision (will flag in summary + amend ROADMAP). Verify manifest shell schema exists before writing plan.


161:## Shell adapters (`kind = "shell"`)
168:kind = "shell"                     # vs "harness"
180:wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive
181:can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly
182:require_approval = "none"          # none | remembered | always — per-spawn user-approval gate (floor; node/endpoint may tighten)
184:over_cap = "reject"                # reject | approve — at the cap: refuse, or require per-spawn approval beyond it (doesn't raise the cap)
198:**Sleep/wake:** when offline (+ `wake_command` set), spt-core runs the wake-watcher (mutually exclusive with the shell binary); `exit(wake-opcode)` → wake resolution; crash-exit → respawn with exponential backoff + give-up. `persistent` auto-onlines the shell with its owner. `can_shutdown` authorizes `api owner-shutdown`. Cross-node *fresh-spawn* to wake an owner requires the owner's `shell_wake_spawn_anywhere` settings flag (pre-consent).
200:[Omitted long matching line]


crates\spt-daemon\src\resting.rs:21://! ([`read_rest`] / [`write_rest`] over [`spt_store::info::set_rest_state`]);
crates\spt-daemon\src\resting.rs:41://! (REQ-INST-6 — deferred rows hold while the instance rests) is **M5**: the
crates\spt-daemon\src\resting.rs:221:        .rest_state
crates\spt-daemon\src\resting.rs:349:    let state = RestState::from_tag(rec.rest_state.as_deref()?)?;
crates\spt-daemon\src\resting.rs:364:    info::set_rest_state(perch_path, state.as_tag(), anchor)
crates\spt-daemon\src\resting.rs:663:        spt_store::info::set_rest_state(d.path(), "hibernating", None).unwrap();
crates\spt-daemon\src\lifecycle.rs:643:                rec.rest_state.as_deref(),
crates\spt-daemon\src\lifecycle.rs:652:            assert_eq!(rec.rest_state.as_deref(), Some("suspended"));


Write

# M5 Plan — shells, presence, deferred capabilities

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a/M3b/M3c/M4-PLAN.md`.
> Task layer authored in full 2026-06-04 at M5 start (see §Tasks).
> Branch: `dev-freeform`. Authoritative architecture: **CONTEXT §Shell model**,
> **CONTEXT §Consent & security gates**, **CONTEXT §Instances**,
> `docs/MANIFEST.md` §Shell adapters (`kind = "shell"`), ADR-0004 (daemon hosting),
> ADR-0009 (gate nesting), and `ROADMAP.md` §M5.

> **Upstream is done:** **M4 COMPLETE (2026-06-04)** — `spt-net` (WAN/pairing/registry),
> the multi-instance model (resting states, resources, home+fork), cross-node Psyche
> sync, WAN msg/remote-drive/xfer, the notif primitive, update peer-propagation, and the
> production trigger loops, proven on the real two-host rig (CI `26958175812`). M5 builds
> the *driven-surface* layer on those substrates: shells ride the broker process table +
> typed/binary payloads; presence rides the registry distribution; consent rides the
> security-material plumbing the access whitelist (REQ-SEC-1) already shaped.

## Scope decisions (user, 2026-06-04 — resolve the ROADMAP §M5 ↔ PRD/DEFERRED forks)

1. **Presence: resolution only.** Presence gossip (`last_active_node, last_active_endpoint, ts`
   subnet-wide) + a first-class most-recently-active resolution consumed by notif/update/
   consent/wake routing. The PresenceChannel *endpoint* (dispatch/bind/thread styles, the
   durable shell-agnostic thread) stays **deferred past v1** (DEFERRED.md stance holds).
2. **Consent: framework seam only.** The real grant store + interactive escalation ship;
   **both gated capabilities stay deferred** — remote command execution (REQ-REACH-2) *and*
   instantiate-anywhere. Consequence: the **remote fork arm** (cross-node `spt fork`
   placement) defers with instantiate-anywhere, despite the ROADMAP §M5 carried-seam
   listing — ROADMAP wording amended at D0.
3. **Shells: machinery + mock + one real shell.** Full shell hosting machinery proven by
   the generic mock shell adapter, **plus one minimal real shell — the OS-notification
   shell** (Windows toast / Linux `notify-send`) as the dogfood proof. This deliberately
   pulls one concrete shell out of the DEFERRED.md concrete-shells row; GameRobot-class
   surfaces stay deferred. Cross-node **spawn** stays deferred (rides instantiate-anywhere);
   cross-node **link** of an existing shell (owner active on another node) is in scope —
   it is part of the locked model ("an agent and its shell can link across nodes").
4. **Process:** plan authored directly from these decisions; further forks surfaced
   per-task as they arise.

## Goal

An agent embodies itself in driven surfaces: `spt shell spawn` mints an owner-exclusive
shell instance hosted by the daemon (broker-launched binary, three typed channels,
approval/cap gates), full sleep/wake from either end (owner cascade + wake-watcher), with
subnet presence resolution routing user-directed traffic to the right place — proven by a
real OS-notification shell rendering an agent's `spt notify` as a native toast on the
user's active node, and by the carried M4 seams going live (deferred-message resting
gate, remote suspend, relay rendezvous, notif-ack→apply orchestration, cross-runner CI).

## Scope

### In

- **Consent framework seam** (CONTEXT §Consent & security gates): grant store
  (`capability × agent × target-node`, target-enforced, subnet-settable, revocable, lives
  with security material near the trust store) + interactive escalation (consent prompt →
  most-recently-active session; allow-once / allow-always / deny) + pre-consent flag
  authoring (`can_shutdown`, `shell_wake_spawn_anywhere` as manifest/settings-authored
  grants). The two gated capability ids are **reserved, not implemented**.
- **Adapter registration lifecycle** (`REQ-INSTALL-4`): `spt adapter add <path|--github>`
  (manifest-first; install-is-first-update via the declared `[update]` avenue), soft
  `remove` + optional `uninstall` template; one command for `kind="harness"` and
  `kind="shell"`; the registered set feeds creation-time adapter selection, shell
  discovery, and the R-UPD-5 ripple.
- **Shell hosting machinery** (`REQ-EP-5` + new `REQ-SHELL-*`): shell perches
  (`perches/<owner>/shells/<adapter>-<n>/`), spawn-mints-instance vs relink/persistent/
  wake online switch, broker-launched binary + `api bind` local-link handshake, the three
  channels (command durable · text+file durable · sensory REST-only never-spooled),
  owner exclusivity, `require_approval` / `max_instances_per_owner` / `over_cap` gates
  (consuming the grant store), broadcast-vs-discovery, aliasing, context injection,
  `shell list/cmd/relink/teardown/rename`.
- **Shell sleep/wake**: offline-on-owner-offline, `persistent` auto-online,
  `wake_command` wake-watcher (exit-opcode supervision, exponential backoff + give-up),
  state-keyed wake resolution, `spt shutdown` owner cascade, `api owner-shutdown`
  (gated by `can_shutdown`).
- **Presence resolution** (new `REQ-PRES-1`): the presence datum gossiped subnet-wide
  (agent-interaction heartbeat; rides registry distribution) + one first-class
  most-recently-active resolution API replacing the per-feature precursors
  (notif first-fire, update consent, escalation, wake resolution).
- **Carried M4 seams**: `REQ-INST-6` deferred-message resting gate (seam documented in
  `spt-daemon::resting`), remote `spt suspend <id@node>` arm, relay rendezvous routing
  (D2f Q1 — consumer now exists), notif-ack → `apply_brain_only` apply orchestration
  (the production caller), cross-runner CI two-host job, KH 7.4
  (`REQ-HAZARD-DAEMON-SCHED-NONBLOCKING`) binding at the per-agent-runtime fan-out.
- **One real shell**: the **OS-notification shell** — minimal `kind="shell"` adapter +
  binary rendering a typed notify command natively; integrates the `notif_command`
  manifest seam (R-NOTIF-2's "render via the attached shell's template" M5 generalization).

### Out

- **PresenceChannel endpoint** (dispatch/bind/thread, durable cross-shell thread) —
  deferred past v1 (decision 1). The presence-resolution API is its future substrate.
- **Remote command execution** (`REQ-REACH-2`) + **instantiate-anywhere** — deferred
  (decision 2); the consent framework ships with their capability ids reserved. The
  **remote fork arm** and **cross-node shell spawn** defer with instantiate-anywhere;
  `shell_wake_spawn_anywhere` ships as grant *shape* only (wake resolution's
  no-reachable-instance branch refuses with a documented deferral).
- **Concrete shells beyond the OS-notification shell** (GameRobot, in-session-inject) —
  deferred (DEFERRED.md row amended, not removed).
- **Cross-user / per-(subnet,user)** generalizations — unchanged forward seams.
- **Shell-binary sandboxing** — unchanged accepted-risk stance (CONTEXT §binary-trust).

## Requirements

Existing inactive reqs that activate per task (rule 5): `REQ-EP-5`, `REQ-INSTALL-4`,
`REQ-INST-6`, `REQ-HAZARD-DAEMON-SCHED-NONBLOCKING` (7.4), `REQ-HAZARD-STALE-INDEX-LOCK`
(1.3 — binds when the daemon owns boot sweeps for the git-touching runtimes it hosts),
`REQ-HAZARD-STDIN-SESSION-ID` (2.2 — binds if/when the M5 adapter api surface grows a
stdin session-id path; re-assess at D3). New reqs to register first (rule 3) at D0:
`REQ-CONSENT-1` (grant store), `REQ-CONSENT-2` (interactive escalation),
`REQ-PRES-1` (presence gossip + MRA resolution), `REQ-SHELL-1` (shell hosting machinery:
channels/lifecycle/exclusivity), `REQ-SHELL-2` (sleep/wake watcher + cascade).
`REQ-INST-1`'s activation rationale (instantiate-anywhere) no longer fires at M5 —
update its rule-5 note at D0 to defer with the capability. `REQ-REACH-2` stays inactive.

## Clean-room posture

Nothing here has a sister-project analog to copy (the sister had no shells, no presence,
no consent store) — clean-room throughout. The OS-notification shell binary is original;
its OS integration uses stock platform surfaces (Windows toast API / `notify-send`).

## Tasks (expanded 2026-06-04 at M5 start)

> Sub-tasks are the executable unit; each lands with its tag in the same commit
> (TRACEABILITY rule 1). Activate listed reqs **at the sub-task that delivers them**
> (rule 5). Conventions carried: no `cargo fmt`; `traceable-reqs check` from repo root
> before declaring done; clippy `-D warnings` on Linux CI is a real gate; push each
> slice → watch the sha-pinned FINAL conclusion.

**Sequencing.** D0 is the doc/registry alignment. D1 (consent) and D2 (adapter
registration) are the floor — D3's gates consume D1's grant store and D3's shell
adapters must be registerable via D2. D4 (sleep/wake) sits on D3. D5 (resting arms) is
independent after M4 and can interleave. D6 (presence) upgrades routing consumers built
through D1/D4 — build the precursor-consuming API late enough to swap call sites once.
D7 (carried infra) is parallelizable; D7c (CI job) early pays off for everything after.
D8 (real shell) needs D2+D3+D4+D6. D9 is the closeout. **Each D-task is itself a
milestone-sized body** — expect a `/self-clear` + commune between most; commit atomically
per sub-task.

### D0 — Scope alignment + requirement registry prep
- **D0a** Amend `ROADMAP.md` §M5 to the decided scope (presence resolution only; consent
  framework seam only; remote-fork-arm + cross-node-spawn defer with instantiate-anywhere;
  mock + OS-notification shell). Amend `docs/DEFERRED.md`: presence-gossip row leaves the
  table (ships M5); concrete-shells row notes the OS-notification carve-out;
  instantiate-anywhere / remote-exec rows note the framework landed M5, capabilities still
  parked. CONTEXT touch-ups only where wording drifts from the above.
- **D0b** Register the new reqs (rule 3, all `required_stages = []` until their sub-task):
  `REQ-CONSENT-1`, `REQ-CONSENT-2`, `REQ-PRES-1`, `REQ-SHELL-1`, `REQ-SHELL-2`. Update
  `REQ-INST-1`'s rule-5 rationale (defers with instantiate-anywhere, not M5).
- **Acceptance:** `traceable-reqs check` green; docs consistent with the scope decisions.

### D1 — Consent framework seam (grant store + escalation)
- **D1a** Grant store: `capability × subject-agent × target-node` rows; **enforced at the
  target node**; settable subnet-wide (replicates as security material near the trust
  store — the REQ-SEC-1 plumbing); revocable. Capability ids `remote-exec` /
  `instantiate-anywhere` reserved-but-refusing; real v1 consumers are the D3 shell gates.
  · **REQ-CONSENT-1**
- **D1b** Interactive escalation: ungated action → consent prompt routed to the user's
  most-recently-active session (the M4 notif-producer path; upgraded to REQ-PRES-1 at
  D6b); **allow-once / allow-always (writes a grant) / deny**. · **REQ-CONSENT-2**
- **D1c** Pre-consent flag authoring: manifest `can_shutdown` + endpoint settings
  `shell_wake_spawn_anywhere` author grants through the same store (different authoring
  path, same model). Flag *shape* only for spawn-anywhere (behavior deferred).
- **Acceptance:** grant present → allowed; absent → escalation; deny blocks; allow-always
  persists + replicates; revocation propagates; gate nesting order (visibility →
  whitelist → grants, ADR-0009) holds in tests.

### D2 — Adapter registration lifecycle
- **D2a** `spt adapter add <path>` / `--github <user/repo>`: manifest-first validation,
  record under `{SPT_HOME}/…/adapters/` (copy for `file_pull`, pointer for `delegated`);
  **install is the first update** through the declared `[update]` avenue; one command for
  both adapter kinds. · **REQ-INSTALL-4 (impl/unit)**
- **D2b** `spt adapter remove`: soft-deregister (hidden from new-creation; live instances
  keep running) + optional manifest `uninstall` template once quiesced (or `--force`).
- **D2c** Wire the registered set as the source for: creation-time adapter selection
  (auto-if-one/ask-if-many), shell discovery (D3d), and the R-UPD-5 ripple set.
- **Acceptance:** add → validate → use → remove lifecycle E2E with mock harness + mock
  shell manifests; a removed adapter's live instance survives.

### D3 — Shell hosting machinery
- **D3a** Shell perch layout (`perches/<owner>/shells/<adapter>-<n>/`; `info.json` carries
  `type=Shell, owner, adapter_name, status, alias?` only — capabilities resolve from the
  manifest). `spt shell spawn <adapter> [--alias]` **mints** a new instance (canonical id
  `<adapter>-<n>`); `relink` / `teardown` / `list` / `cmd` / `rename`. Spawn:create ::
  relink/persistent/wake:online. · **REQ-EP-5 (impl/unit begins)**
- **D3b** Broker-launched binary (never user-launched) + `api bind` (type=Shell) with the
  **local-link handshake** (link token + channel encryption on every message); the three
  command-delivery modes (HTTP / stdin-via-broker / child-relay `api poll`).
- **D3c** The three content channels over the typed/binary substrate: **command**
  (agent→shell, durable/spooled), **text+file** (2-way durable; transfer
  progress-queryable), **sensory** (`api emit --type <t>`, shell→agent, **REST-only,
  never spooled**, dropped unless the owner is live). · **REQ-SHELL-1**
- **D3d** Gates + discovery: `require_approval` (`none|remembered|always`; manifest floor,
  node/endpoint may tighten) and `max_instances_per_owner` + `over_cap` consume the D1
  grant store (`spawn-shell × agent × (node, shell-adapter)` grants); broadcast policy
  (`subnet|same-node|none`) governs **discovery only**; discovery scope = own instances +
  instantiable adapters; owner exclusivity enforced; shell awareness injected into the
  agent's context. · **REQ-EP-5 (gates + discovery)**
- **D3e** Per-agent-runtime fan-out scheduling: the daemon now hosts many per-agent
  loops/processes (shells, watchers, pulse/Psyche) — each agent's bounded blocking work
  off the shared scheduler; one slow agent must not stall another's tick.
  · **REQ-HAZARD-DAEMON-SCHED-NONBLOCKING (7.4)**; boot-time stale-lock sweep for the
  git-touching runtimes the daemon hosts · **REQ-HAZARD-STALE-INDEX-LOCK (1.3)**;
  re-assess **REQ-HAZARD-STDIN-SESSION-ID (2.2)** — activate iff a stdin session-id api
  path now exists, else leave with an updated rationale.
- **Acceptance:** mock-shell E2E spawn → bind+handshake → command → sensory → teardown;
  non-owner refused; `remembered`/`always` approval + `over_cap` enforced through real
  grants; alias addressing works; `--no-default-features` lib build stays clean.

### D4 — Shell sleep/wake + owner cascade
- **D4a** Lifecycle: link-break **always** closes broker link + binary (manifest pre-close
  instruction + termination timeout); **ephemeral** ⇒ full teardown + history erase;
  **persistent** ⇒ perch offline, re-linkable (binary re-spawned on relink);
  offline-on-owner-offline; `persistent` auto-onlines with the owner. · **REQ-SHELL-2 begins**
- **D4b** `wake_command` wake-watcher: runs **while offline** (mutually exclusive with the
  binary), on the shell's node; **exit(wake-opcode)** → wake resolution; any other exit →
  respawn with exponential backoff + eventual give-up; one watcher per offline instance.
- **D4c** State-keyed wake resolution: owner dormant → online the shell; suspended →
  revive owner, then online; active elsewhere → attach/online; **no reachable instance →
  no-op with a documented refusal** (the `shell_wake_spawn_anywhere` fresh-spawn branch
  stays deferred with instantiate-anywhere; the D1c grant shape is its seam).
  · **REQ-SHELL-2**
- **D4d** `spt shutdown` (agent gracefully suspends its own endpoint: suspend boundary
  signoff fires, persistent shells cascade offline) + `api owner-shutdown` (shell
  suspends its owner directly, gated by the `can_shutdown` pre-consent grant).
- **Acceptance:** full sleep/wake cycle driven from both ends (owner shutdown → shell
  offline → watcher up → wake opcode → owner revived → shell online); crash-exit
  backoff observed; ephemeral-vs-persistent divergence proven.

### D5 — Resting-state arms (carried M4 seams)
- **D5a** Deferred-message resting gate: deferred (spool-only) rows **hold** while the
  instance is dormant/suspended, **release on the wake edge** (the documented
  `spt-daemon::resting` seam; extends KH 1.4/4.4 with the instance-state gate).
  · **REQ-INST-6**
- **D5b** Remote `spt suspend <id[@node]>` + remote `spt wake` completeness (suspend/wake
  any of your instances from any node — ungated, same trust class as remote-drive).
- **D5c** Remote fork arm: **document the deferral** (rides instantiate-anywhere,
  decision 2) — no build.
- **Acceptance:** deferred message held while resting, delivered exactly once after wake;
  remote suspend/wake proven cross-node (loopback int + rig leg at D9).

### D6 — Presence resolution
- **D6a** Presence datum `(last_active_node, last_active_endpoint, ts)` gossiped
  subnet-wide — agent-interaction heartbeat as the signal (the `last_active_ms` recency
  stamp generalized), riding the registry/notif distribution; visibility-gated like
  registry data. · **REQ-PRES-1 begins**
- **D6b** One first-class **most-recently-active resolution API**; swap the per-feature
  precursors to it: notif first-fire, update-consent delivery, consent escalation (D1b),
  wake resolution (D4c). · **REQ-PRES-1**
- **D6c** Document the PresenceChannel endpoint as the deferred consumer (dispatch/bind/
  thread styles build on this datum + the broker PresenceLog seam, REQ-EP-4) — seam note,
  no build.
- **Acceptance:** activity shifting nodes redirects notif/consent delivery cross-node;
  loopback int + rig leg at D9.

### D7 — Carried infra seams
- **D7a** Relay rendezvous routing (M4-D2f Q1): route the pre-trust pairing rendezvous
  via the relay using the existing `H(subnet-name ‖ TOTP-epoch)` token derivation
  (`spt-net::pairing::rendezvous`) — pairing beyond LAN/known-addr; the deferral's
  "no consumer until shells" trigger has fired.
- **D7b** Notif-ack → `apply_brain_only` orchestration: consent-notif acknowledgment
  drives the staged update apply — the **production caller** for `apply_brain_only`
  (closes the M4-D9 "no-production-caller" annotation).
- **D7c** Cross-runner CI two-host job: automate the `[twohost]` ladder as a cross-runner
  workflow (hfenduleam + gravity runners coordinated in one run; runbook stays the manual
  fallback). Gotchas #7/#13/#17 discipline baked in.
- **Acceptance:** relay-routed pairing E2E; a real update applies off a notif ack;
  the cross-runner job goes green on the rig unattended.

### D8 — The real shell: OS-notification shell (dogfood)
- **D8a** Minimal `kind="shell"` adapter + original binary: renders a typed notify
  command as a native OS notification (Windows toast / Linux `notify-send`);
  `broadcast = "same-node"`, `persistent = true`, sane `require_approval` default;
  registered via D2, hosted via D3/D4.
- **D8b** `notif_command` integration (R-NOTIF-2's M5 generalization): when presence
  resolves the user to an endpoint with this shell attached, a subnet notif renders via
  the shell's template — `spt notify` → native toast on the user's active node.
- **D8c** Cross-node owner↔shell **link**: owner active on node B relinks its persistent
  shell living on node A (commands ride Iroh; spawn stays same-node per decision 2).
- **Acceptance:** the dogfood demo — an agent's `spt notify` surfaces as a real OS toast
  on whichever rig host the user last touched; cross-node relink proven.

### D9 — Activation sweep + two-host E2E + closeout
- **D9a** Rig int legs (env-gated `[twohost]`, runbook-extended; or via the D7c job):
  remote suspend/wake · presence-shift redirect · cross-node shell relink + drive ·
  notif→toast. Tag `[int->REQ-*]` on the gated tests.
- **D9b** Sweep: every M5 req's `required_stages` satisfied or rule-5-deferred with
  rationale; `traceable-reqs check` green; amend ROADMAP (M5 delivered) + CONTEXT +
  DEFERRED; CI matrix (ubuntu + windows + traceability + cross-runner) green FINAL.
- **Acceptance:** scope decisions 1–3 demonstrably met; closeout recorded.

## Requirement → task coverage map

| Task | Activates |
|------|-----------|
| D0 | registers REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 (inactive); REQ-INST-1 rationale update |
| D1 | REQ-CONSENT-1, REQ-CONSENT-2 |
| D2 | REQ-INSTALL-4 |
| D3 | REQ-EP-5, REQ-SHELL-1, REQ-HAZARD-DAEMON-SCHED-NONBLOCKING, REQ-HAZARD-STALE-INDEX-LOCK (+ REQ-HAZARD-STDIN-SESSION-ID iff path exists) |
| D4 | REQ-SHELL-2 |
| D5 | REQ-INST-6 |
| D6 | REQ-PRES-1 |
| D7 | (no new activations — closes M4 deferral annotations) |
| D8 | REQ-EP-5/SHELL-1/2 + REQ-NOTIF-2 `int` evidence via the real shell |
| D9 | int sweep + closeout |

**Stays deferred (out of M5):** REQ-REACH-2, instantiate-anywhere (+ remote fork arm,
cross-node shell spawn, `shell_wake_spawn_anywhere` behavior), the PresenceChannel
endpoint (REQ-EP-4 stays at its M4 broker-seam stage), GameRobot-class shells,
cross-user generalizations, REQ-MIGRATE-1.