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

# M4 Plan — networking + instances

> **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. Confirm with user at D6 build start before locking the conflict-surface UX.
- **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.
- **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).


<owl_messages>
<owl_message from="doyle-psyche" priority="normal"><EVENT type="echo_commune" from="doyle-psyche" timestamp="2026-06-03T21:29:58-07:00" machine="HFENDULEAM" project="spt-core" branch="dev-freeform" head_sha="f49aba1984a28ca066345bfe218b839d5a5ff8e0" head_subject="feat(sec): M4-D5d endpoint access whitelist (REQ-SEC-1, ADR-0009)" note="Echo commune brief — auto-fired">&lt;live-context&gt;
Generation 13 closing. D5 COMPLETE — all four slices CI-green: D5a caab433, D5b 4ea3b37, D5c 269b8ef(+fcad41c fix), D5d f49aba1. Next: D6 Psyche sync.

## D5d COMPLETE (f49aba1, CI-green both legs)
- spt-store/access.rs: AccessStore @ identity/trust/access.json — absent=open, present=restrict, empty entry=locked down; allow/revoke/open (revoke never widens; open = only explicit return to default-open); users[] inert ADR-0006 reservation. RecentOutbound correlation log: durable (CLI proc ≠ daemon brain proc), 15-min window, 512 cap, refresh-in-place.
- spt-daemon/access.rs gate swapped (callers born gated at D5a — zero caller churn): same-node short-circuit (non-minting node.key read; NO OnceLock cache — with_home tests flip SPT_HOME per-test in-process) → AccessStore lookup per check (live `spt access` edits apply next inbound, no restart) → class split (Reply exempt). classify_inbound + note_outbound exported. Policy: only wan.rs message funnel classifies reply-vs-unsolicited; attach/xfer control surfaces ALWAYS Unsolicited.
- CLI: spt access allow|revoke|open|list; 64-hex subject validation (refuse typos — unmatched subject = silent lockout).
- tests/access.rs wire test: non-listed unsolicited Refused (no spool) → note_outbound → same node passes as Reply (stateful firewall) → allow → admitted mid-stream. Same-node + decision table unit-tested in src/access.rs. REQ-SEC-1 [impl,unit] active.

## Gotchas (carry permanently)
1. Journaled net_stream_send (Some(op_id)) blocks for ack + DISCARDS interleaved events. Pump loops use None; exactly-once at edges.
2. Echo children repeat typed input; compare against local OutputLog replay.
3. CI Linux clippy rustc 1.96 deny-defaults newer than local clippy.
4. &quot;Stop once buffer contains needle&quot; readers overshoot differently per event batching. Byte-identical asserts compare prefix-through-first-needle (attach.rs::prefix_through).
5. Brain::snapshot()/handoff() panic without PTY session — net-only restart tests use fresh cold_start + resubscribe.
6. QUIC bidi stream INVISIBLE to acceptor until first bytes flow — send before wait_for_stream, or accept loop hangs.
7. `gh run watch` exit code LIES — verify FINAL via `gh run list --json conclusion`.

## Current Focus: D6 — Cross-node Psyche sync (P2P), retire gh-repo interim
NO JIT PLAN YET — next generation drafts M4-D6-PLAN.md first. Inputs: M4-PLAN.md §D6 (lines ~159-178), ADR-0002 (hub stays opt-in tracked/ only), docs/CONTEXT-MEMORY.md, CONTEXT.md §cross-instance context freshness + §visibility gates sync.
- D6a: P2P context replication replacing gh-repo sync. 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 — model DECIDED 2026-06-03: per-node monotonic epoch counters + version vector (unifies w/ D3b epoch-lease source); dominate→accept, dominated→drop, concurrent→SURFACE CONFLICT never silent newest-wins; wall-clock only human tiebreaker hint. ⚠ MUST CONFIRM merge-conflict-surface UX WITH USER at D6b build start before locking (M4-PLAN note).
- D6c: subnet-exclusive sync (hidden ⟹ not synced; per-endpoint membership list). Extend REQ-HAZARD-DIRECT-WRITE-PRECEDENCE to multi-writer/node-id.
- Maintenance rule: D6 fault rows into FAULT-MATRIX same change.

## After D6
D7.5 → D8 (notif; &quot;node X driving&quot; light becomes notif-producer consuming D5c event seam) → D9 two-host integration (REQ-NET-1 int stage; chaos pass planned rows P1-P3/P6/P7).

## Loose Threads
- Production `spt send` WAN leg not CLI-wired (daemon funnel + records exist; D5a tests only). note_outbound call site wires when send leg lands. Revisit by D9.
- D5c driven_by marker stale across hard brain crash w/o re-serve (documented; successor re-serve re-stamps).

## Flake Ledger
1. brain_restart_survives_gaplessly — hung once hosted Windows; timeout bounds it.
2. (resolved fcad41c) attach byte-identical overshoot race.

## CI Topology (stable)
- hfenduleam (Windows): C:\actions-runner detached (NOT service — dies on reboot; re-launch: Start-Process C:\actions-runner\run.cmd).
- gravity (Linux): systemd service, auto-start.
- CI warm: persistent runner target/ (8.2G hfenduleam); Build ~60s Win / ~2m15s Linux. checkout clean does NOT wipe it (verified empirically).

## Workflow Constraints (carried)
- NO cargo fmt
- Tag [impl/unit-&gt;REQ-…] on real evidence only
- traceable-reqs check from REPO ROOT (exit 0 before push)
- Linux CI clippy -D warnings is real gate
- Verify FINAL CI conclusion via gh run list --json conclusion (gotcha #7)
- Push each slice → watch green before next
- Commit trailer: Co-Authored-By: Claude Opus 4.8 (1M context) &lt;noreply@anthropic.com&gt;

## No Blockers (D6b UX confirm is a scheduled user touchpoint, not a blocker)
&lt;/live-context&gt;
&lt;project-context&gt;
D5 complete: WAN messages, remote attach, remote-drive detection + file transfer, access whitelist — all CI-green. D6 Psyche sync next; JIT plan to draft; D6b merge-conflict UX needs user confirmation at build start.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


1	# Psyche context & memory model
2
3	> Draft design doc (2026-05-31). Resolves PRD §17.3 (Psyche live/project authoring directives) and the memformat redesign. Governs how the Psyche authors, structures, syncs, and surfaces an agent's context. On-disk layout: `docs/STORAGE.md`. Several patterns are adapted from `BigscreenVR/pi-agent-memory` (credited inline).
4
5	## Two-tier context
6
7	- **live context** (per-agent, project-independent — the agent's *mind*): identity/persona, relationship + rapport with the user, the user's cross-project preferences & working style, cross-project life threads, **learnings from work that may apply to other projects**, a thin **project roster** (one line per project the agent works on), the agent's self-model. Synced to **all** instances of the endpoint.
8	- **project context** (per-agent-per-project — what the agent is doing *here*): codebase/architecture, decisions made here, **current focus / next steps / todos**, project conventions, build/test commands, blockers. Synced **only** to same-project instances.
9
10	(This split independently matches pi-agent-memory's Project Store vs Global Store and its discriminator dialogue — convergent validation.)
11
12	### The discriminator (applied per *topic*, not per fragment)
13
14	> **"Would this still be true and relevant if the user opened a *different* project tomorrow?"** Yes → live. No → project. Equivalently: **WHO the agent is** (live) vs **WHAT it's doing here** (project).
15
16	### Anti-leak rules
17
18	The observed failure is project detail leaking *into* live context (which then wrongly syncs everywhere). Countermeasures:
19
20	1. **Default-to-project.** A fragment goes to project context unless it *passes the cross-project test* to be promoted to live. Promotion needs justification; demotion is free. The bias points against the leak.
21	2. **`current focus / next steps` is project-scoped** — the single biggest leak category. Route it to project unless the focus genuinely spans projects.
22	3. **Live holds a thin project roster, never project depth** — cross-project *awareness* travels; depth stays per-project.
23	4. **Commune-source asymmetry** (the structural fix): the live signal is strong only from **Self-authored communes**; **echo communes are project-centric** (they synthesize project-heavy session logs). So **echo communes are project-context-primary and live-context-conservative** — they make few/no live edits. This kills the leak at its source. Provenance is recorded on each block (`Source:`).
24
25	### Boundary communes are Self-authored (resume Self, not echo)
26
27	To get meaningful **live-context** updates on a consistent basis (the live signal is strong only from Self), three boundaries author their commune by **`--resume`-ing the Self session** (the actual agent), not by a background haiku echo:
28
29	- **`/clear`** → resume Self → **commune** file-drop.
30	- **`/compact`** → resume Self → **commune** file-drop.
31	- **`spt suspend`** → resume Self → **signoff** file-drop (suspend = the endpoint going down, so it's a signoff, not a commune).
32
33	This deliberately swaps the project-centric echo for a Self-authored capture at exactly the moments that matter (context loss + suspend), so live context stays current. (Routine cadence communes remain echo — project-primary, live-conservative.)
34
35	### `spt refresh` (spt-hosted self-service context refresh)
36
37	A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-clear resume (it can't guarantee that for harness-hosted sessions it doesn't control).
38
39	### Ownership
40
41	The live/project authoring directives are **core-owned** — fed to every Psyche by spt-core (part of `$psyche_prompt` / a core system-prompt fragment), so the split is consistent across all adapters (inconsistent rules → sync chaos). Adapters add only harness mechanics (how to write the drop file).
42
43	## memformat (deeply integrated, self-evolving, the split scaffold)
44
45	memformat goes from a decorative, ignored seed to the **load-bearing schema that defines the two-tier context**. It makes the live/project decision **per-topic and rare** instead of per-fragment and constant — which is what makes the split stable.
46
47	- **Tier-tagged topics.** Each topic carries a tier: `live` or `project`. The tier is decided **once, when the topic is created** (apply the discriminator one time), not per fragment. Communes then just *fill* topics; the tier routes content to the right context file + sync tier. The two-slice envelope (`<live-context>`/`<project-context>`) becomes the *rendering* of tier-tagged topics.
48	- **Self-evolving.** The Psyche adds/removes/re-tiers topics as it learns Self's realm of work (memformat's original intent). **Re-tiering migrates** the topic's accumulated content between tiers (project→live or vice versa), not forward-only.
49	- **The schema is live** (per-agent, travels everywhere); **project-tagged topic content** is instantiated per-project.
50	- Example tiers: `identity / relationship / user-preferences / cross-project-learnings / project-roster` = **live**; `focus / decisions / architecture / todos / blockers` = **project**.
51
52	### Structured blocks (adapted from pi-agent-memory)
53
54	A memformat topic is a tier-tagged **container of structured blocks**, not free-form prose. Each block:
55
56	```
57	## <stable-block-id>
58	Concepts: <comma list>          # retrieval keys
59	Aliases: <comma list>           # retrieval synonyms (optional)
60	Applies to: <glob paths>        # sub-project scoping (optional)
61	Source: self-commune | echo-commune | user   # provenance (drives the live-conservative rule)
62	Created: <iso>
63	Updated: <iso>
64	Hash: sha256:<canonical-content-hash>         # for block-level OCC merge (deferred, see below)
65	Pinned: true|false              # hot (dense-injected) vs indexed (sparse, on-demand)
66
67	<body>
68	```
69
70	`Concepts`/`Aliases` make context **retrievable**; `Applies to` enables fine-grained scoping + query-routing; `Source` carries provenance; `Pinned` drives start-injection density (below).
71
72	## Retrieval Map (generated INDEX)
73
74	A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: "which endpoint owns this task") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.
75
76	## Start-injection: dense working set, sparse tail
77
78	The balance between immediate productivity and bounded context cost — tuned for SPT's **per-agent, work-fast** priority (vs pi's multi-agent-per-project digging, which is sparse-by-default):
79
80	- **DENSE — the working set, injected in full at start:** all **live context** + the **current project's hot blocks** (`Pinned` + recently-updated focus/decisions/todos). The agent is immediately productive, no digging.
81	- **SPARSE — the long tail, pointer + INDEX, retrieved on demand:** other projects, **cross-agent/cross-project synthesis**, cold/archived blocks. (This is where pi's bounded "topic-names + pointer, never inline bodies" awareness pattern applies — it scales regardless of store size.)
82	- **Hot/cold curated by the Psyche** via `Pinned` + recency; keeping the hot set tight is part of maintaining memformat.
83	- **Tunable** via a working-set **density knob** (spt-core global setting + per-endpoint override).
84
85	## Deferred
86
87	- **Rebalance / Memory-Health pass** (later): the Psyche periodically (a) demotes leaked live→project content, and (b) — the deeper, pi-credited insight — verifies context is still *true vs the current code* (Memory Health), which requires investigating the codebase, so it's an autonomous-agent job, distinct from mechanical staleness (hash/INDEX mismatch).
88	- **Block-level OCC merge** (v1.x): the per-block `Hash:` (canonical content hash, excluding `Updated`/`Hash`) enables block-level, stale-aware cross-node merge — finer than the v1 whole-file newest-wins driver (`docs/STORAGE.md`). Recorded now (the block format carries `Hash:`), applied later.
89
90	## Not adopted
91
92	pi-agent-memory's **human-in-the-loop approval** (extract → review → approve, max-5-candidates) and **bash-mutation guardrails** fit its "human curates" philosophy. SPT's Psyche is **trusted to author its own mind autonomously** — so we adopt pi's *structure* (blocks, INDEX, bounded awareness, provenance, OCC) but **not** its approval gate. Controlled mutation is already covered by the drop-file single-writer rule (KNOWN-HAZARDS 6.4).
93

# Psyche context & memory model

> Draft design doc (2026-05-31). Resolves PRD §17.3 (Psyche live/project authoring directives) and the memformat redesign. Governs how the Psyche authors, structures, syncs, and surfaces an agent's context. On-disk layout: `docs/STORAGE.md`. Several patterns are adapted from `BigscreenVR/pi-agent-memory` (credited inline).

## Two-tier context

- **live context** (per-agent, project-independent — the agent's *mind*): identity/persona, relationship + rapport with the user, the user's cross-project preferences & working style, cross-project life threads, **learnings from work that may apply to other projects**, a thin **project roster** (one line per project the agent works on), the agent's self-model. Synced to **all** instances of the endpoint.
- **project context** (per-agent-per-project — what the agent is doing *here*): codebase/architecture, decisions made here, **current focus / next steps / todos**, project conventions, build/test commands, blockers. Synced **only** to same-project instances.

(This split independently matches pi-agent-memory's Project Store vs Global Store and its discriminator dialogue — convergent validation.)

### The discriminator (applied per *topic*, not per fragment)

> **"Would this still be true and relevant if the user opened a *different* project tomorrow?"** Yes → live. No → project. Equivalently: **WHO the agent is** (live) vs **WHAT it's doing here** (project).

### Anti-leak rules

The observed failure is project detail leaking *into* live context (which then wrongly syncs everywhere). Countermeasures:

1. **Default-to-project.** A fragment goes to project context unless it *passes the cross-project test* to be promoted to live. Promotion needs justification; demotion is free. The bias points against the leak.
2. **`current focus / next steps` is project-scoped** — the single biggest leak category. Route it to project unless the focus genuinely spans projects.
3. **Live holds a thin project roster, never project depth** — cross-project *awareness* travels; depth stays per-project.
4. **Commune-source asymmetry** (the structural fix): the live signal is strong only from **Self-authored communes**; **echo communes are project-centric** (they synthesize project-heavy session logs). So **echo communes are project-context-primary and live-context-conservative** — they make few/no live edits. This kills the leak at its source. Provenance is recorded on each block (`Source:`).

### Boundary communes are Self-authored (resume Self, not echo)

To get meaningful **live-context** updates on a consistent basis (the live signal is strong only from Self), three boundaries author their commune by **`--resume`-ing the Self session** (the actual agent), not by a background haiku echo:

- **`/clear`** → resume Self → **commune** file-drop.
- **`/compact`** → resume Self → **commune** file-drop.
- **`spt suspend`** → resume Self → **signoff** file-drop (suspend = the endpoint going down, so it's a signoff, not a commune).

This deliberately swaps the project-centric echo for a Self-authored capture at exactly the moments that matter (context loss + suspend), so live context stays current. (Routine cadence communes remain echo — project-primary, live-conservative.)

### `spt refresh` (spt-hosted self-service context refresh)

A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-clear resume (it can't guarantee that for harness-hosted sessions it doesn't control).

### Ownership

The live/project authoring directives are **core-owned** — fed to every Psyche by spt-core (part of `$psyche_prompt` / a core system-prompt fragment), so the split is consistent across all adapters (inconsistent rules → sync chaos). Adapters add only harness mechanics (how to write the drop file).

## memformat (deeply integrated, self-evolving, the split scaffold)

memformat goes from a decorative, ignored seed to the **load-bearing schema that defines the two-tier context**. It makes the live/project decision **per-topic and rare** instead of per-fragment and constant — which is what makes the split stable.

- **Tier-tagged topics.** Each topic carries a tier: `live` or `project`. The tier is decided **once, when the topic is created** (apply the discriminator one time), not per fragment. Communes then just *fill* topics; the tier routes content to the right context file + sync tier. The two-slice envelope (`<live-context>`/`<project-context>`) becomes the *rendering* of tier-tagged topics.
- **Self-evolving.** The Psyche adds/removes/re-tiers topics as it learns Self's realm of work (memformat's original intent). **Re-tiering migrates** the topic's accumulated content between tiers (project→live or vice versa), not forward-only.
- **The schema is live** (per-agent, travels everywhere); **project-tagged topic content** is instantiated per-project.
- Example tiers: `identity / relationship / user-preferences / cross-project-learnings / project-roster` = **live**; `focus / decisions / architecture / todos / blockers` = **project**.

### Structured blocks (adapted from pi-agent-memory)

A memformat topic is a tier-tagged **container of structured blocks**, not free-form prose. Each block:

```
## <stable-block-id>
Concepts: <comma list>          # retrieval keys
Aliases: <comma list>           # retrieval synonyms (optional)
Applies to: <glob paths>        # sub-project scoping (optional)
Source: self-commune | echo-commune | user   # provenance (drives the live-conservative rule)
Created: <iso>
Updated: <iso>
Hash: sha256:<canonical-content-hash>         # for block-level OCC merge (deferred, see below)
Pinned: true|false              # hot (dense-injected) vs indexed (sparse, on-demand)

<body>
```

`Concepts`/`Aliases` make context **retrievable**; `Applies to` enables fine-grained scoping + query-routing; `Source` carries provenance; `Pinned` drives start-injection density (below).

## Retrieval Map (generated INDEX)

A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: "which endpoint owns this task") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.

## Start-injection: dense working set, sparse tail

The balance between immediate productivity and bounded context cost — tuned for SPT's **per-agent, work-fast** priority (vs pi's multi-agent-per-project digging, which is sparse-by-default):

- **DENSE — the working set, injected in full at start:** all **live context** + the **current project's hot blocks** (`Pinned` + recently-updated focus/decisions/todos). The agent is immediately productive, no digging.
- **SPARSE — the long tail, pointer + INDEX, retrieved on demand:** other projects, **cross-agent/cross-project synthesis**, cold/archived blocks. (This is where pi's bounded "topic-names + pointer, never inline bodies" awareness pattern applies — it scales regardless of store size.)
- **Hot/cold curated by the Psyche** via `Pinned` + recency; keeping the hot set tight is part of maintaining memformat.
- **Tunable** via a working-set **density knob** (spt-core global setting + per-endpoint override).

## Deferred

- **Rebalance / Memory-Health pass** (later): the Psyche periodically (a) demotes leaked live→project content, and (b) — the deeper, pi-credited insight — verifies context is still *true vs the current code* (Memory Health), which requires investigating the codebase, so it's an autonomous-agent job, distinct from mechanical staleness (hash/INDEX mismatch).
- **Block-level OCC merge** (v1.x): the per-block `Hash:` (canonical content hash, excluding `Updated`/`Hash`) enables block-level, stale-aware cross-node merge — finer than the v1 whole-file newest-wins driver (`docs/STORAGE.md`). Recorded now (the block format carries `Hash:`), applied later.

## Not adopted

pi-agent-memory's **human-in-the-loop approval** (extract → review → approve, max-5-candidates) and **bash-mutation guardrails** fit its "human curates" philosophy. SPT's Psyche is **trusted to author its own mind autonomously** — so we adopt pi's *structure* (blocks, INDEX, bounded awareness, provenance, OCC) but **not** its approval gate. Controlled mutation is already covered by the drop-file single-writer rule (KNOWN-HAZARDS 6.4).


Grep

cross-instance|freshness|visibility gates sync|gh-repo|gh repo


72:Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps) or **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them.
423:**cross-instance context freshness** (two-tier):
430:The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.
443:- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.


Echo commune from own Psyche — FYI only, matches known state. Continuing D6 input reading.


400	- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
401	- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.
402
403	### Pieces the Instances model requires
404
405	**subnet registry**:
406	Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.
407
408	**endpoint visibility (per-(endpoint, subnet))**:
409	Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.
410
411	**home subnet (per endpoint)**:
412	The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).
413
414	**resource advertisement (subnet resource registry)**:
415	A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt resources set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).
416
417	**resolution policy**:
418	How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).
419
420	**instantiate-anywhere** (seam day-one, behavior deferred):
421	From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.
422
423	**cross-instance context freshness** (two-tier):
424	When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
425	- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
426	- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.
427
428	So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.
429
430	The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.
431
432	**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.
433
434	### Off-node reach-back
435
436	When the user is driving an instance from off-node, the agent should be able to:
437	- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
438	- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
439	- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)
440
441	### v1 scope line (V1-mid)
442
443	- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
444	- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
445	- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.
446
447	## Workspace & versioning
448
449	The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).
450
451	Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).
452
453	**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns.
454
455	**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.
456
457	## Pairing & trust
458
459	How a single user's own nodes come to trust each other and form a subnet. Cross-*user* subnet (two *different* users' subnets interoperating) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept), but the model is **built with the seam for it** (see *subnet membership*).

- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.

### Pieces the Instances model requires

**subnet registry**:
Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.

**endpoint visibility (per-(endpoint, subnet))**:
Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.

**home subnet (per endpoint)**:
The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).

**resource advertisement (subnet resource registry)**:
A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt resources set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).

**resolution policy**:
How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).

**instantiate-anywhere** (seam day-one, behavior deferred):
From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.

**cross-instance context freshness** (two-tier):
When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.

So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.

The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.

**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.

### Off-node reach-back

When the user is driving an instance from off-node, the agent should be able to:
- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)

### v1 scope line (V1-mid)

- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.

## Workspace & versioning

The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).

Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).

**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns.

**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.

## Pairing & trust

How a single user's own nodes come to trust each other and form a subnet. Cross-*user* subnet (two *different* users' subnets interoperating) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept), but the model is **built with the seam for it** (see *subnet membership*).


CONTEXT.md:409:[Omitted long matching line]
crates\spt-store\src\visibility.rs:16://! enter or resolve from a subnet's registry), and **visibility gates sync** —
crates\spt-store\src\visibility.rs:153:    /// (fail-closed). And **visibility gates sync** (ADR-0006 §6): `hidden ⟹ not
docs\adr\0006-multi-subnet-membership-model.md:29:6. **Visibility = "excluded", not "unlisted".** Hidden in subnet S means **not advertised AND not routable** from S (a real boundary, ready for the cross-user seam), not merely absent from `list`. Hidden iff `S.hide_new_endpoints` (per-subnet, captured at join time) OR `E.default_hide_from_new_subnets` (per-endpoint), unless an explicit per-`(E,S)` override wins. Both defaults ship OFF (visible). **Visibility gates sync:** `hidden ⟹ not synced`.
M4-D3-PLAN.md:60:   override; **visibility gates sync**. Resolution/advertise honor excluded.
M4-PLAN.md:131:  routable; OR-of-defaults + per-`(E,S)` override; **visibility gates sync**. · **REQ-INST-12, REQ-INST-13**
PRD.md:115:- **R-INST-12** (M4, ADR-0006) **Endpoint visibility (per-(endpoint, subnet)):** hidden means **excluded** — not advertised *and not routable* from that subnet (a real boundary), not merely unlisted. Hidden iff per-subnet `hide_new_endpoints` (captured at join time) **OR** per-endpoint `default_hide_from_new_subnets`, unless an explicit per-(E,S) override wins; both defaults OFF. **Visibility gates sync.**
traceable-reqs.toml:208:title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
traceable-reqs.toml:213:[Omitted long matching line]


Found 4 files
traceable-reqs.toml
M4-PLAN.md
CONTEXT.md
PRD.md


M4-D2-WIRE-PLAN.md
M4-D2f-PLAN.md
M4-D2g-PLAN.md
M4-D3-PLAN.md
M4-D4-PLAN.md
M4-D5-PLAN.md


211-- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
212-- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.
213-- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.
214-- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).
215-
216:### 6.5 Direct-write precedence guard against stale LLM overwrites
217-- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
218-- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.
219-- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity, and the freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the distributed generalization of this guard. Highest-value carryover for the sync design.
220-- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.
221-
--
277-| 5.3 | Timeout every harness subprocess | manifest invocations |
278-| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
279-| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
280-| 6.1 | Single path/registry source of truth | storage layout |
281-| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
282:| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
283-| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
284-| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
285-| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |
286-| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |
287-| 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) |