80-  the recency pick. `consent.rs` keeps its public surface; the resolver core
81-  moves to the notif engine and consent's becomes the degenerate
82-  (unscoped) call.
83:- **Resurface = engine + boundary wiring, wake edge deferred.** One engine
84-  function `resurface_at_boundary(endpoint, boundary)` filters: undismissed ∧
85-  endpoint ∉ `seen` ∧ `now − last_surfaced_ms > suppression` (default
86-  3600 000 ms, a `notif.rs` const with a config seam) ∧ endpoint visible in
--
106-- **When feeds pump is NOT this task.** The production replication trigger
107-  loop (when a node emits/receives notif feeds) rides D9 with every other
108-  loop. D8 ships the drivers: emit-side record authoring, apply-side
109:  merge, E2E over loopback broker streams — same deferral as D5/D6/D7.
110-
111-## Slices (each CI-green before the next)
112-
--
210-  `spt notify`: the ADR-0007 forward seam, M5; the `from` field ships ready.
211-- **Concrete `notif_command` adapters** (OS toast, GameRobot `alert-symbol`):
212-  M5 — D8 ships the manifest seam + render only.
213:- **PresenceChannel** — the deferred generalization of the delivery step;
214-  the notif engine is its registry-resolution precursor, unchanged scope.
215-- **GUI surfaces** (notif badges etc.) — out of spt-core.
216-


M4-D7-PLAN.md:59:- **VerifyPolicy is caller-supplied** (pure posture, same as C1/C2): the
M4-D7-PLAN.md:60:  `request_update` driver takes `&VerifyPolicy`. Production policy assembly


CONTEXT.md:444:- **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.
M4-D6-PLAN.md:1:# M4-D6 — cross-node Psyche sync (P2P), retire gh-repo interim (JIT plan)
M4-D6-PLAN.md:14:daemon dispatch). Remaining D6 exit: D9 two-host int legs; gh-repo interim sync
M4-D6-PLAN.md:61:  gh-repo-sync skill retires after the D9 two-host proof.
M4-PLAN.md:36:- **Cross-node Psyche sync (P2P)** — replaces the interim gh-repo sync; the direct-write
M4-PLAN.md:159:### D6 — Cross-node Psyche sync (P2P), retire gh-repo interim


40	  `verify_metadata` on the offer **before fetching any artifact bytes** — a
41	  rollback / expired / off-channel / bad-sig offer is rejected without pulling
42	  the binary. Only a metadata-verified offer proceeds to `Fetch` → `Chunk`* →
43	  `Done`, then `plan_verified` closes the loop on the real bytes (D7b: the gate
44	  is per-node, never inherited from the sender's claim to have verified).
45	- **Staging = becoming a server (the self-heal mechanism).** A release that
46	  passed `plan_verified` is staged into the node's release cache
47	  (`SPT_HOME`-anchored dir: `release.json` = the `SignedRelease`, `artifact.bin`
48	  = the bytes; atomic temp→rename, same posture as D5c commit). `serve_update`
49	  answers queries from the staged release. Verified-pull ⇒ staged ⇒ servable:
50	  propagation chains A→B→C with the gate at every hop. **Unverified bytes are
51	  never staged** — there is no code path from wire bytes to the cache except
52	  through `plan_verified`.
53	- **Serve gate: trusted peers only, fail-closed.** A query is answered only when
54	  the handshake-proven origin is trusted in ≥1 subnet (`TrustStore`, the same
55	  check as the D6c `p-` tier). Anything else gets `UpToDate`-shaped silence —
56	  refuse by not offering.
57	- **Torn pull = re-query.** No resume protocol: staging is atomic, chunks are
58	  positional, the query is idempotent. FAULT-MATRIX row.
59	- **VerifyPolicy is caller-supplied** (pure posture, same as C1/C2): the
60	  `request_update` driver takes `&VerifyPolicy`. Production policy assembly
61	  (trusted-key store, channel pin, running version) rides the D9 daemon
62	  lifecycle with the trigger loops — same deferral as the D5/D6 serve loops.
63
64	## Slices (each CI-green before the next)
65
66	1. **D7-1 — wire records + release cache.**
67	   - `spt-net/src/net/update.rs`: `UpdRecord` enum — `Query { upd_id,
68	     current_version, channel }`, `Offer { upd_id, metadata_json, signature_hex }`,
69	     `UpToDate { upd_id }`, `Fetch { upd_id }`, `Chunk { upd_id, offset,
70	     data_b64 }`, `Done { upd_id, total }`, `Err { upd_id, message }`;
71	     `UpdDecoder = NdjsonDecoder<UpdRecord>`. Unit tests mirror `sync.rs`:
72	     round-trip all kinds, unknown-kind skip, split-record reassembly, forged
73	     origin-field inert (`[unit->REQ-HAZARD-WAN-ORIGIN-AUTH]`).
74	   - `spt-daemon/src/relcache.rs`: `ReleaseCache` — `stage(signed, artifact)`
75	     (atomic temp→rename pair), `staged() -> Option<(SignedRelease, PathBuf)>`,
76	     `load_artifact()`. Unit tests: stage/load round-trip, restage replaces,
77	     torn temp file ignored.
78	   - Activate **REQ-UPD-1 → `[impl, unit]`** in `traceable-reqs.toml`; tag
79	     evidence in both files.
80	2. **D7-2 — serve/request drivers + loopback propagation E2E.**
81	   - `spt-daemon/src/propagate.rs`:
82	     `serve_update(brain, stream_id, origin_node, net_from_seq, cache, trust)`
83	     — trust-gate the origin, answer Query from the staged release (Offer or
84	     UpToDate), pump chunks on Fetch, Done; Detached/Failed outcomes mirroring
85	     `SyncServeOutcome`.
86	     `request_update(brain, conn_id, open_op, policy, running, cache, scratch)
87	     -> io::Result<UpdatePullOutcome>` — Query → gate the Offer's metadata
88	     (reject pre-fetch) → Fetch → reassemble in scratch → `plan_verified` →
89	     stage → return `Updated(UpdatePlan)` / `UpToDate` /
90	     `Rejected(RejectReason)` (loud + typed, never silent — the §D failure
91	     mode).
92	   - Int tests (`tests/propagate.rs`, two/three loopback brokers — copy the
93	     `tests/sync.rs` harness incl. **stream-table snapshot before the requester
94	     opens**, gotcha: the table holds locally-opened streams too):
95	     - happy path: B (v5) pulls v6 from A; gate passes; plan is `BrainOnly`;
96	       staged bytes byte-identical (`[unit->REQ-UPD-1]`).
97	     - **chain self-heal**: A→B→C — C pulls from B *after* B staged; gate ran
98	       at B and again at C (`[unit->REQ-UPD-1]`).
99	     - **tampered relay**: B's staged artifact bytes are corrupted on disk; C's

  `verify_metadata` on the offer **before fetching any artifact bytes** — a
  rollback / expired / off-channel / bad-sig offer is rejected without pulling
  the binary. Only a metadata-verified offer proceeds to `Fetch` → `Chunk`* →
  `Done`, then `plan_verified` closes the loop on the real bytes (D7b: the gate
  is per-node, never inherited from the sender's claim to have verified).
- **Staging = becoming a server (the self-heal mechanism).** A release that
  passed `plan_verified` is staged into the node's release cache
  (`SPT_HOME`-anchored dir: `release.json` = the `SignedRelease`, `artifact.bin`
  = the bytes; atomic temp→rename, same posture as D5c commit). `serve_update`
  answers queries from the staged release. Verified-pull ⇒ staged ⇒ servable:
  propagation chains A→B→C with the gate at every hop. **Unverified bytes are
  never staged** — there is no code path from wire bytes to the cache except
  through `plan_verified`.
- **Serve gate: trusted peers only, fail-closed.** A query is answered only when
  the handshake-proven origin is trusted in ≥1 subnet (`TrustStore`, the same
  check as the D6c `p-` tier). Anything else gets `UpToDate`-shaped silence —
  refuse by not offering.
- **Torn pull = re-query.** No resume protocol: staging is atomic, chunks are
  positional, the query is idempotent. FAULT-MATRIX row.
- **VerifyPolicy is caller-supplied** (pure posture, same as C1/C2): the
  `request_update` driver takes `&VerifyPolicy`. Production policy assembly
  (trusted-key store, channel pin, running version) rides the D9 daemon
  lifecycle with the trigger loops — same deferral as the D5/D6 serve loops.

## Slices (each CI-green before the next)

1. **D7-1 — wire records + release cache.**
   - `spt-net/src/net/update.rs`: `UpdRecord` enum — `Query { upd_id,
     current_version, channel }`, `Offer { upd_id, metadata_json, signature_hex }`,
     `UpToDate { upd_id }`, `Fetch { upd_id }`, `Chunk { upd_id, offset,
     data_b64 }`, `Done { upd_id, total }`, `Err { upd_id, message }`;
     `UpdDecoder = NdjsonDecoder<UpdRecord>`. Unit tests mirror `sync.rs`:
     round-trip all kinds, unknown-kind skip, split-record reassembly, forged
     origin-field inert (`[unit->REQ-HAZARD-WAN-ORIGIN-AUTH]`).
   - `spt-daemon/src/relcache.rs`: `ReleaseCache` — `stage(signed, artifact)`
     (atomic temp→rename pair), `staged() -> Option<(SignedRelease, PathBuf)>`,
     `load_artifact()`. Unit tests: stage/load round-trip, restage replaces,
     torn temp file ignored.
   - Activate **REQ-UPD-1 → `[impl, unit]`** in `traceable-reqs.toml`; tag
     evidence in both files.
2. **D7-2 — serve/request drivers + loopback propagation E2E.**
   - `spt-daemon/src/propagate.rs`:
     `serve_update(brain, stream_id, origin_node, net_from_seq, cache, trust)`
     — trust-gate the origin, answer Query from the staged release (Offer or
     UpToDate), pump chunks on Fetch, Done; Detached/Failed outcomes mirroring
     `SyncServeOutcome`.
     `request_update(brain, conn_id, open_op, policy, running, cache, scratch)
     -> io::Result<UpdatePullOutcome>` — Query → gate the Offer's metadata
     (reject pre-fetch) → Fetch → reassemble in scratch → `plan_verified` →
     stage → return `Updated(UpdatePlan)` / `UpToDate` /
     `Rejected(RejectReason)` (loud + typed, never silent — the §D failure
     mode).
   - Int tests (`tests/propagate.rs`, two/three loopback brokers — copy the
     `tests/sync.rs` harness incl. **stream-table snapshot before the requester
     opens**, gotcha: the table holds locally-opened streams too):
     - happy path: B (v5) pulls v6 from A; gate passes; plan is `BrainOnly`;
       staged bytes byte-identical (`[unit->REQ-UPD-1]`).
     - **chain self-heal**: A→B→C — C pulls from B *after* B staged; gate ran
       at B and again at C (`[unit->REQ-UPD-1]`).
     - **tampered relay**: B's staged artifact bytes are corrupted on disk; C's


1	# M4-D6 — cross-node Psyche sync (P2P), retire gh-repo interim (JIT plan)
2
3	**Status:** D6a COMPLETE (D6a-1 `8b42452`, D6a-2 `1ef9802` — CI-green); D6b COMPLETE
4	(D6b-1 `7e9c2b5` CI-green: marker v2 + vectors + decide_merge + conflict artifacts +
5	hazard 6.6; D6b-2 `50620e4`: bounded turn driver (D7.5a) + reconcile_file +
6	is_reconciler election). D6c COMPLETE (D6c-1 `f08aedc`: BranchStore bundle plumbing
7	+ `syncmerge::apply_fetched` per-file vector driver, REQ-NET-3 activated; D6c-2:
8	SyncRecord wire + SyncPolicy server gate + serve_sync/request_sync/select_refs +
9	reconcile_after_sync election wiring + loopback two-daemon E2E + FAULT-MATRIX rows
10	14–17, REQ-INST-5 activated). **Production trigger loops** (context-write notify →
11	peers pull; dormant→active freshness pull; periodic anti-entropy) are thin calls
12	over `select_refs` + `request_sync` once the daemon lifecycle holds peer conns —
13	wired at D9 with the two-host topology (the same deferral as the D5 serve loops'
14	daemon dispatch). Remaining D6 exit: D9 two-host int legs; gh-repo interim sync
15	retires after the D9 proof.
16	Drafted 2026-06-03 at D6 start; all design forks resolved with user → **ADR-0013**;
17	decisions mirrored into M4-PLAN §D6b/§D7.5a, CONTEXT.md, STORAGE.md, KNOWN-HAZARDS
18	6.5/6.6.
19
20	## Goal
21
22	The mind follows the user across machines: an endpoint's **live context** replicates to
23	all its instances, **project context** only to same-project instances (loopback
24	two-daemon in CI; two-host at D9). A concurrent cross-node write is **surfaced and
25	Psyche-reconciled, never silently dropped**. Reqs: REQ-NET-3, REQ-INST-2, REQ-INST-5,
26	REQ-STORE-1, REQ-HAZARD-DIRECT-WRITE-PRECEDENCE (distributed generalization).
27
28	## Decisions (locked — ADR-0013, user-confirmed 2026-06-03)
29
30	- **Merge order = version vectors, wall-clock never orders.** Marker grows
31	  `spt:node=<node_id> spt:vector=<node:epoch,…>`; entries from each node's monotonic
32	  `EpochSource` (`spt-store/src/epoch.rs` — the same counter behind the D3b lease, the
33	  planned unification). Dominate→accept, dominated→drop, concurrent→surface. A
34	  missing/legacy vector = ⊥ (dominated by any vector; two ⊥s fall back to the local
35	  6.5 window rule).
36	- **Concurrent → Psyche auto-reconcile, single reconciler.** Both versions persist as
37	  **tracked conflict artifacts** (deterministic content-hash names — idempotent,
38	  replicate like any context file; local working file untouched). The **active
39	  instance's node** runs one bounded, stdin-fed, **stdout-captured** Psyche turn over
40	  both versions; fallback reconciler = lowest node id among holders. Merged write =
41	  `join(vA,vB)` + reconciler bump — dominates both parents, clears artifacts
42	  subnet-wide. Turn bounded (5.3) + output validated (non-empty, marker-parseable);
43	  any failure leaves artifacts for retry at next sync/activation + fires the event
44	  seam (D8 notif producer later). Reconciled output is `source=llm` — the 6.5
45	  direct-write window still applies.
46	- **D7.5a pulled forward into D6b.** The bounded stdout-captured Psyche turn driver
47	  (NOT the `Stdio::null()` interim, 7.3) is built here; D7.5 keeps b/c (taxonomy +
48	  relay/sanitize). Driver code tagged `[impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]` as
49	  evidence, but that req **activates at D7.5** (full invariant needs the relay).
50	- **Transport = git-native bundles over broker QUIC streams; the DAG is shared.**
51	  Pull-based, **ref-scoped**: a node requests `a-<id>` for endpoints with
52	  `synced(E,S)` (the D3e gate, finally consumed) and `p-<project>` only for projects
53	  it hosts — two-tier scoping falls out of the pull model, no new registry fields.
54	  Responder builds an incremental `git bundle` (`^have` exclusions); requester fetches
55	  from it + merges with the vector driver; first contact merges unrelated seeds once
56	  (`--allow-unrelated-histories`). Divergent per-node histories rejected.
57	- **BranchStore v1 = git-CLI-backed** (system `git`; the `git pull` handoff seam
58	  already assumes it) behind a trait so gitoxide is a later drop-in. Commit-per-write
59	  latency measured in D6a tests (the ADR-0011 spike, empirically).
60	- **Hub mode (gh opt-in) stays a deferred seam** — D6 is P2P only. Interim
61	  gh-repo-sync skill retires after the D9 two-host proof.
62
63	## Pieces (build order)
64
65	1. **D6a — context store realization** (REQ-STORE-1, REQ-INST-2).
66	   `spt-store::branchstore`: ensure-repo/ensure-branch, **commit = checkpoint, tip =
67	   resume**, linked worktree views with relative paths (`worktree.useRelativePaths`,
68	   Git ≥ 2.48; else `git worktree repair` fallback — STORAGE §Worktree portability).
69	   `tracked/` layout + path fns in `perch.rs` (`agents/<id>/live-context.md` on
70	   `a-<id>`; `projects/<pid>/<id>/project-context.md` on `p-<pid>` —
71	   REQ-HAZARD-SINGLE-PATH-SOURCE tags). `project_id` derivation (git remote URL →
72	   folder-name fallback, STORAGE §project_id). Ingest migration: commune drops'
73	   `<live-context>`/`<project-context>` slices route into the tier files
74	   (precedence-guarded writes, `ingest.rs` folds there instead of flat `context.md`);
75	   resume/preload reads follow. `spt rename` context-branch arm (the REQ-INST-11
76	   seam noted at D3f: `a-<old>` → `a-<new>`).
77	2. **D6b — distributed precedence + Psyche reconcile** (extends
78	   REQ-HAZARD-DIRECT-WRITE-PRECEDENCE; new hazard req below).
79	   `VersionVector` type + dominance (pure, hermetic); marker v2 parse/render
80	   (back-compat: v1 markers = ⊥ vector); per-file vector merge fn; conflict artifact

# M4-D6 — cross-node Psyche sync (P2P), retire gh-repo interim (JIT plan)

**Status:** D6a COMPLETE (D6a-1 `8b42452`, D6a-2 `1ef9802` — CI-green); D6b COMPLETE
(D6b-1 `7e9c2b5` CI-green: marker v2 + vectors + decide_merge + conflict artifacts +
hazard 6.6; D6b-2 `50620e4`: bounded turn driver (D7.5a) + reconcile_file +
is_reconciler election). D6c COMPLETE (D6c-1 `f08aedc`: BranchStore bundle plumbing
+ `syncmerge::apply_fetched` per-file vector driver, REQ-NET-3 activated; D6c-2:
SyncRecord wire + SyncPolicy server gate + serve_sync/request_sync/select_refs +
reconcile_after_sync election wiring + loopback two-daemon E2E + FAULT-MATRIX rows
14–17, REQ-INST-5 activated). **Production trigger loops** (context-write notify →
peers pull; dormant→active freshness pull; periodic anti-entropy) are thin calls
over `select_refs` + `request_sync` once the daemon lifecycle holds peer conns —
wired at D9 with the two-host topology (the same deferral as the D5 serve loops'
daemon dispatch). Remaining D6 exit: D9 two-host int legs; gh-repo interim sync
retires after the D9 proof.
Drafted 2026-06-03 at D6 start; all design forks resolved with user → **ADR-0013**;
decisions mirrored into M4-PLAN §D6b/§D7.5a, CONTEXT.md, STORAGE.md, KNOWN-HAZARDS
6.5/6.6.

## Goal

The mind follows the user across machines: an endpoint's **live context** replicates to
all its instances, **project context** only to same-project instances (loopback
two-daemon in CI; two-host at D9). A concurrent cross-node write is **surfaced and
Psyche-reconciled, never silently dropped**. Reqs: REQ-NET-3, REQ-INST-2, REQ-INST-5,
REQ-STORE-1, REQ-HAZARD-DIRECT-WRITE-PRECEDENCE (distributed generalization).

## Decisions (locked — ADR-0013, user-confirmed 2026-06-03)

- **Merge order = version vectors, wall-clock never orders.** Marker grows
  `spt:node=<node_id> spt:vector=<node:epoch,…>`; entries from each node's monotonic
  `EpochSource` (`spt-store/src/epoch.rs` — the same counter behind the D3b lease, the
  planned unification). Dominate→accept, dominated→drop, concurrent→surface. A
  missing/legacy vector = ⊥ (dominated by any vector; two ⊥s fall back to the local
  6.5 window rule).
- **Concurrent → Psyche auto-reconcile, single reconciler.** Both versions persist as
  **tracked conflict artifacts** (deterministic content-hash names — idempotent,
  replicate like any context file; local working file untouched). The **active
  instance's node** runs one bounded, stdin-fed, **stdout-captured** Psyche turn over
  both versions; fallback reconciler = lowest node id among holders. Merged write =
  `join(vA,vB)` + reconciler bump — dominates both parents, clears artifacts
  subnet-wide. Turn bounded (5.3) + output validated (non-empty, marker-parseable);
  any failure leaves artifacts for retry at next sync/activation + fires the event
  seam (D8 notif producer later). Reconciled output is `source=llm` — the 6.5
  direct-write window still applies.
- **D7.5a pulled forward into D6b.** The bounded stdout-captured Psyche turn driver
  (NOT the `Stdio::null()` interim, 7.3) is built here; D7.5 keeps b/c (taxonomy +
  relay/sanitize). Driver code tagged `[impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]` as
  evidence, but that req **activates at D7.5** (full invariant needs the relay).
- **Transport = git-native bundles over broker QUIC streams; the DAG is shared.**
  Pull-based, **ref-scoped**: a node requests `a-<id>` for endpoints with
  `synced(E,S)` (the D3e gate, finally consumed) and `p-<project>` only for projects
  it hosts — two-tier scoping falls out of the pull model, no new registry fields.
  Responder builds an incremental `git bundle` (`^have` exclusions); requester fetches
  from it + merges with the vector driver; first contact merges unrelated seeds once
  (`--allow-unrelated-histories`). Divergent per-node histories rejected.
- **BranchStore v1 = git-CLI-backed** (system `git`; the `git pull` handoff seam
  already assumes it) behind a trait so gitoxide is a later drop-in. Commit-per-write
  latency measured in D6a tests (the ADR-0011 spike, empirically).
- **Hub mode (gh opt-in) stays a deferred seam** — D6 is P2P only. Interim
  gh-repo-sync skill retires after the D9 two-host proof.

## Pieces (build order)

1. **D6a — context store realization** (REQ-STORE-1, REQ-INST-2).
   `spt-store::branchstore`: ensure-repo/ensure-branch, **commit = checkpoint, tip =
   resume**, linked worktree views with relative paths (`worktree.useRelativePaths`,
   Git ≥ 2.48; else `git worktree repair` fallback — STORAGE §Worktree portability).
   `tracked/` layout + path fns in `perch.rs` (`agents/<id>/live-context.md` on
   `a-<id>`; `projects/<pid>/<id>/project-context.md` on `p-<pid>` —
   REQ-HAZARD-SINGLE-PATH-SOURCE tags). `project_id` derivation (git remote URL →
   folder-name fallback, STORAGE §project_id). Ingest migration: commune drops'
   `<live-context>`/`<project-context>` slices route into the tier files
   (precedence-guarded writes, `ingest.rs` folds there instead of flat `context.md`);
   resume/preload reads follow. `spt rename` context-branch arm (the REQ-INST-11
   seam noted at D3f: `a-<old>` → `a-<new>`).
2. **D6b — distributed precedence + Psyche reconcile** (extends
   REQ-HAZARD-DIRECT-WRITE-PRECEDENCE; new hazard req below).
   `VersionVector` type + dominance (pure, hermetic); marker v2 parse/render
   (back-compat: v1 markers = ⊥ vector); per-file vector merge fn; conflict artifact


<owl_messages>
<owl_message from="doyle-psyche" priority="normal"><EVENT type="echo_commune" from="doyle-psyche" timestamp="2026-06-04T02:02:11-07:00" machine="HFENDULEAM" project="spt-core" branch="dev-freeform" head_sha="b8d75af3cf12d899aff2b1af51caf44482d141a6" head_subject="docs(plan): mark M4-D8 complete (all four slices CI-green)" note="Echo commune brief — auto-fired">&lt;live-context&gt;
Generation 15. D8 COMPLETE — CI-green @ b8d75af. D9 (M4 closeout) next.

## D8 COMPLETE (ADR-0007)
- 9a6548f D8-1: NotifRow/NotifStore (identity/notifs.db). Merge model = join-semilattice (NOT D6b vector — deliberate): notif_id = `&lt;node-hex&gt;:&lt;epoch&gt;`, dismissed = OR latch, seen = endpoint-set union, last_surfaced_ms = max. Idempotent + commutative; wall-clock only inside suppression heuristic.
- f612b3f D8-2: NotifRecord NDJSON (no origin — KH 7.5) + notifsync.rs emit/apply fail-closed gate (member subnet ∧ trusted origin IN that subnet; subject = handshake-proven stream-table origin). Push-feed posture (D4d, not D6c pull): replay IS recovery. FAULT-MATRIX rows 26–29.
- 4548410 D8-3: notif.rs engine — most_recently_active_where, first_fire → most-recently-active LIVE VISIBLE endpoint (hidden never targets), resurface_at_boundary (seen-per-endpoint + ~1h cross-endpoint suppression on max-merged last_surfaced_ms). produce_consent_notif wraps REQ-UPD-4. psyrelay notify swapped onto primitive. REQ-NOTIF-1 [impl,unit].
- 126d843 D8-4: spt notify CLI (from=`&lt;id&gt;@&lt;node-hex&gt;`; subnet never guessed) + spt notif list/dismiss + [session.notif] manifest role. REQ-NOTIF-2 [doc,impl,unit].
- b8d75af: M4-D8-PLAN.md plan-status.

Design invariants (carry):
- Notif state monotone — never add non-monotone field to NotifRow without revisiting merge model.
- Replication gate = handshake origin, never payload (KH 7.5); trust is per-subnet.
- Visibility gates surfacing; seen-per-endpoint gates re-show; dismissal = only ack.
- Psyche notify: subnet daemon-supplied via PsycheNotifCtx, never Psyche markup.

## Current Focus: D9 — activation sweep + cross-node E2E + closeout
Read M4-PLAN §D9 + deferred list first → draft M4-D9-PLAN.md.
- D9a: dormancy resource budget (red-team #9, REQ-INST-3/4).
- D9b: two-host E2E on real rig (gravity-linux ↔ Windows): pair → register → cross-node message → remote-drive → Psyche sync → update self-heal.
- D9c: REQ-INST-14/15 register-or-document.
- D9d: full required_stages sweep + traceable-reqs green + ROADMAP/CONTEXT amend + CI matrix green.

## Full Deferred-to-D9 List
- Production trigger loops: sync, update, psyche-turn, notif feeds.
- Production VerifyPolicy assembly (trusted-key store, channel pin, running version).
- spt send WAN leg CLI wiring.
- gh-repo interim sync retirement.
- Wake-edge (state→active) resurface hook.
- Notif feed-pump production loop.
- REQ-NOTIF int legs (two-host).
- Adapter-payload propagation: post-v1.
- p- tier per-file visibility filtering: post-v1.
- D5c driven_by stale across hard crash (documented).

## Gotchas (carry permanently — all 10)
1. Journaled net_stream_send (Some(op_id)) blocks ack + DISCARDS interleaved events. Pump loops use None.
2. Echo children repeat typed input; compare against local OutputLog replay.
3. CI Linux clippy rustc 1.96 deny-defaults newer than local.
4. &quot;Stop once buffer contains needle&quot; overshoots; compare prefix-through-first-needle (prefix_through).
5. Brain::snapshot()/handoff() panics without PTY session — net-only restart tests use fresh cold_start.
6. QUIC bidi stream invisible to acceptor until first bytes flow — send before wait_for_stream.
7. `gh run watch` exit code LIES — verify FINAL via `gh run list --json conclusion`.
8. Manifest test commands in TOML '…' literals — nested single quotes break parse; Windows (cmd /C) masks it.
9. Broker stream table holds LOCALLY-OPENED outbound streams too — snapshot before requester opens; stale stream → Detached → hung forever.
10. Scripted `&lt;EVENT&gt;` envelopes in tests: raw `\n` splits line — use `&lt;br&gt;` per 4.1 grammar.

## Flake Ledger
1. brain_restart_survives_gaplessly — hung once hosted Windows; timeout bounds it.
2. spt-term::digest_parses_a_real_pty_stream — failed once on gravity; rerun passed.
3. sync.rs torn_pull + concurrent_writes — HARDENED: wait_for_stream_except 2s→10s.

## CI Topology (stable)
- hfenduleam (Windows): C:\actions-runner detached (NOT service — re-launch: `Start-Process C:\actions-runner\run.cmd`). Queue can sit minutes — poll, don't panic.
- gravity (Linux): systemd service, auto-start.

## 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 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
&lt;/live-context&gt;
&lt;project-context&gt;
D7 complete b67d432/9b5c39c CI-green (REQ-UPD-1 [impl, unit]; int = D9 two-host). M4 remaining: D7.5 (Psyche outbound relay) → D8 (subnet notif primitive) → D9 (two-host int legs + production trigger loops + gh-interim retire). Conventions hold: no cargo fmt; evidence-only [impl/unit→REQ-…] tags; traceable-reqs check; clippy -D warnings; push slice → watch FINAL via gh run list; commit trailer Claude Opus 4.8 (1M context).
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


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-required_stages = []
173-[[requirements]]
174:id = "REQ-INST-4"
175-title = "active to dormant/suspended fires a transition echo commune"
176-required_stages = []
177-[[requirements]]
178-id = "REQ-INST-5"
--
211-id = "REQ-INST-13"
212-title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
213-[Omitted long context line]
214-[[requirements]]
215:id = "REQ-INST-14"
216-title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
217-required_stages = []
218-[[requirements]]
219:id = "REQ-INST-15"
220-title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
221-required_stages = []
222-[[requirements]]
223-id = "REQ-REACH-1"


4-# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
5-# for the development contract).
6-#
7:# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`)
--
14-
15-[policy]
16-# Default policy applied at activation: doc + impl + unit. Networking/lifecycle
17:# reqs add "int". Until a req is activated, its per-req `required_stages = []`
18-# keeps it out of the coverage gate.
19:required_stages = []
--
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 = []
--
161-[[requirements]]
162-id = "REQ-INST-1"
163-title = "endpoint ID vs instance split (adapter-agnostic ID)"
164:required_stages = []
--
169-[[requirements]]
170-id = "REQ-INST-3"
171-title = "Dormant (warm) / suspended (cold) resting states"
172:required_stages = []
173-[[requirements]]
174-id = "REQ-INST-4"
175-title = "active to dormant/suspended fires a transition echo commune"
176:required_stages = []
--
181-[[requirements]]
182-id = "REQ-INST-6"
183-title = "Deferred messages not delivered to dormant/suspended instances"
184:required_stages = []
--
214-[[requirements]]
215-id = "REQ-INST-14"
216-title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
217:required_stages = []
218-[[requirements]]
219-id = "REQ-INST-15"
220-title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
221:required_stages = []
--
226-[[requirements]]
227-id = "REQ-REACH-2"
228-title = "Remote command execution (deferred, consent-gated)"
229:required_stages = []
--
290-[[requirements]]
291-id = "REQ-PAIR-7"
292-title = "Subnet icon (inline image metadata, GUI-only consumer)"
293:required_stages = []
--
349-[[requirements]]
350-id = "REQ-FRONT-1"
351-title = "Day-one launcher/manager frontend (list/launch/attach/init)"
352:required_stages = []
--
355-[[requirements]]
356-id = "REQ-INSTALL-1"
357-title = "Two install paths; signed one-line script; OS-service registration"
358:required_stages = []
359-[[requirements]]
360-id = "REQ-INSTALL-2"
361-title = "Marketplace-repackaging-friendly install"
362:required_stages = []
363-[[requirements]]
364-id = "REQ-INSTALL-3"
365-title = "Idempotent + interactive-optional first run"
366:required_stages = []
367-[[requirements]]
368-id = "REQ-INSTALL-4"
369-title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"
370:required_stages = []
371-[[requirements]]
372-id = "REQ-MIGRATE-1"
373-title = "Auto-detect and migrate a legacy claude_skill_owl install"
374:required_stages = []
375-[[requirements]]
376-id = "REQ-INFRA-1"
377-title = "GitHub issue tracking for v1; tangled.org as migration target"
378:required_stages = []
--
381-[[requirements]]
382-id = "REQ-DOCS-1"
383-title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
384:required_stages = []
385-[[requirements]]
386-id = "REQ-DOCS-2"
387-title = "Sub-10-minute runnable killer quickstart per audience"
388:required_stages = []
389-[[requirements]]
390-id = "REQ-DOCS-3"
391-title = "Diátaxis structure; one canonical way to do X"
392:required_stages = []
393-[[requirements]]
394-id = "REQ-DOCS-4"
395-title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
396:required_stages = []
397-[[requirements]]
398-id = "REQ-DOCS-5"
399-title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
400:required_stages = []
--
415-[[requirements]]
416-id = "REQ-HAZARD-STALE-INDEX-LOCK"
417-title = "Sweep stale lockfiles on daemon boot (1.3)"
418:required_stages = []
--
427-[[requirements]]
428-id = "REQ-HAZARD-PARENT-PID-PREFER"
429-title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
430:required_stages = []
431-[[requirements]]
432-id = "REQ-HAZARD-STDIN-SESSION-ID"
433-title = "Stdin session_id precedence over env (2.2)"
434:required_stages = []
--
561-[[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:required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops


12:Drive an agent on machine A from machine B (loopback two-daemon in CI; two-host at D9):
25:  deferred-row semantics (1.4/4.4) and codec ordering (4.1) all inherited, not rebuilt.
40:  deferred).
67:   default-open. `spt send` grows the off-node leg transparently. Loopback test: message
95:  the D9 two-host E2E.
137:- Remote command execution (REQ-REACH-2), instantiate-anywhere + consent UX — deferred
141:- Two-host integration — D9 (loopback shapes only here).


Echo commune received — full deferred list confirmed. Gathering remaining context for D9 plan draft.


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

# 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


Bash

ls crates/spt-daemon/src/ crates/spt-daemon/tests/ 2>/dev/null

List daemon source and test files


crates/spt-daemon/src/:
access.rs
adapter_update.rs
attach.rs
brain.rs
broker.rs
codec.rs
config.rs
consent.rs
daemon.rs
digest.rs
digesthub.rs
effect.rs
endpoint.rs
frame.rs
lib.rs
lifecycle.rs
msg.rs
nethost.rs
notif.rs
notifsync.rs
propagate.rs
psyrelay.rs
reconcile.rs
relay.rs
relcache.rs
release.rs
seedmap.rs
sync.rs
test_home.rs
transport.rs
update.rs
wan.rs
xfer.rs

crates/spt-daemon/tests/:
access.rs
attach.rs
brain_swap.rs
broker.rs
daemon_e2e.rs
digest.rs
handoff.rs
idempotent.rs
netbroker.rs
netstream.rs
notifsync.rs
presence.rs
propagate.rs
replicate.rs
sync.rs
wanmsg.rs
xfer.rs


448:    /// Bridges sync→async via the host's own runtime (the broker's dispatch
471:    /// survives — see `Broker::dispatch_net_dial`).


head -60 crates/spt-daemon/src/nethost.rs crates/spt-daemon/src/lifecycle.rs

Read nethost and lifecycle file heads


==> crates/spt-daemon/src/nethost.rs <==
//! The broker-owned network host (D4a) — ADR-0004 §B made real for the wire.
//!
//! [`NetHost`] is the broker's ownership of the node's live network presence:
//! the iroh endpoint ([`spt_net::net::endpoint::NetEndpoint`]) and every
//! accepted/dialed QUIC connection. A live cross-node connection is exactly the
//! kind of resource ADR-0004 §B assigns to the broker — a consumer would lose
//! continuity on brain restart — so the endpoint is constructed *here*, in the
//! stable kernel, never in the brain. The brain drives net operations over the
//! versioned IPC ([`crate::msg::KIND_NET_STATUS`] / [`crate::msg::KIND_NET_DIAL`])
//! and may detach/reattach freely; connections persist in the broker's table.
//!
//! ## Runtime placement (M4-D4-PLAN locked decision)
//!
//! The broker is otherwise sync/thread-based; iroh requires tokio. So the host
//! owns a **dedicated multi-thread tokio runtime** on named worker threads
//! (`spt-broker-net`): QUIC objects live in structures only the runtime's tasks
//! and the host's own `block_on` bridges touch, and the runtime drops with the
//! host (broker teardown stops all net tasks). Spike #3's broker binary proved
//! exactly this shape — a broker-owned endpoint surviving brain kill/restart.
//!
//! ## Ownership closes two §B rows by construction
//!
//! `MdnsAddressLookup` and the relay session are both constructed *inside* the
//! iroh endpoint, so broker ownership of the endpoint closes the ADR-0004 §B
//! "mDNS socket / relay session" row with no extra machinery: there is no
//! separate socket to own.
// [impl->REQ-DAEMON-2]

use std::collections::{HashMap, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use spt_net::net::endpoint::{
    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
    SendStream, SPT_NET_ALPN,
};
use spt_proto::identity::{Identity, PublicKey};

use crate::broker::SharedSend;
use crate::codec::write_frame;
use crate::msg::{
    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,
    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DISCONNECTED,
};

/// The reserved [`crate::effect::EffectKey`] session namespace for net-scoped
/// effects (a dial has no PTY session). Broker session ids are minted from 1
/// (`Broker::next_id` starts at 1), so `0` can never collide with a real session.
pub const NET_EFFECT_SESSION: u64 = 0;

/// Everything needed to stand the broker's endpoint up: the node's identity
/// (the endpoint *is* the node's name — EndpointId == pubkey), the relay stance
/// (from `daemon.json` in production; [`RelayPolicy::Disabled`] in hermetic
/// tests), and whether mDNS LAN discovery rides along.
pub struct NetConfig {
    /// The node identity the endpoint binds as (seed → same ed25519 keypair).
    pub identity: Identity,
    /// Relay stance (ADR-0002): n0 default / self-host / disabled.
    pub relay: RelayPolicy,

==> crates/spt-daemon/src/lifecycle.rs <==
//! The brain's LiveAgent lifecycle host (B3) — the consolidated re-host of the
//! interim `api listen` runner's live half.
//!
//! In the no-daemon interim model the long-lived `spt api listen` process hosted
//! the LiveAgent lifecycle (Psyche spawn, pulse-driven drop ingest, echo fires,
//! graceful signoff) directly in the agent's session. Consolidation
//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the
//! brain runs it as scheduled in-process loops, with a durable, configurable
//! pulse period ([`crate::config::DaemonConfig`]) instead of the interim 5s
//! constant. The `spt` binary's `LiveHost` becomes a thin shim over this type —
//! a re-host, not a rewrite: the heavy lifting stays in the already-pure
//! [`spt_live`] seams ([`spawn_psyche`], [`ingest_drops`], [`run_echo_commune`],
//! [`graceful_signoff`], [`sweep_stale_signoff`]); the ordering invariants
//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live
//! where they always have.
// [impl->REQ-DAEMON-1]

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use spt_live::{
    fetch_history, graceful_signoff, ingest_drops, run_echo_commune, spawn_psyche,
    sweep_stale_signoff, HistoryRecord, Ingested, PsycheHandle, SignoffOutcome,
};
use spt_runtime::{Manifest, ManifestRuntime};
use spt_store::perch::{self, ParentHint};
use spt_store::proc::is_process_alive;

use crate::config::DaemonConfig;

/// Bound for a bounded history/echo invocation in the host (carried from M2b).
const LIVE_BOUNDED_TIMEOUT: Duration = Duration::from_secs(30);

/// Wall-clock now in epoch millis — precedence-marker stamp for ingested writes.
fn now_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// ISO-ish stamp for the psyche prompt (opaque to the Psyche; just a timestamp).
fn now_stamp() -> String {
    format!("epoch-ms:{}", now_ms())
}

/// One pulse tick's observed effects: any drops ingested + whether an echo fired.
#[derive(Debug, Clone, Default)]
pub struct TickReport {
    pub ingested: Vec<Ingested>,
    pub echo_fired: bool,
}

/// The daemon-hosted LiveAgent lifecycle: the runtime + watched drop dirs built
/// from a live-capable manifest, paced by a config-driven [`DaemonConfig`].
pub struct BrainLifecycle {
    runtime: ManifestRuntime,
    id: String,


AskUserQuestion

Question: D9b two-host E2E — how to run it? The two CI runners (HFENDULEAM Windows + gravity Linux) ARE the rig. Cross-runner CI orchestration is possible but rendezvous between two GH Actions jobs over the real network is flaky territory. Alternative: env-gated int tests (e.g. SPT_TWO_HOST=1) + a scripted runbook run manually once on the rig, evidence captured with [int->REQ-*] tags on those tests.

Choices:
Env-gated tests + manual runbook (Recommended)
Two-host int tests behind env gate, skipped in CI. Run once manually on rig; int evidence = real test runs. CI keeps loopback coverage. No flaky cross-runner choreography.
Workflow with paired jobs on both self-hosted runners that rendezvous over the network. Repeatable forever, but coordination + firewall/NAT flake risk in CI.
D9 ships env-gated tests + manual proof; a cross-runner CI job becomes a documented M5 seam.

Question: D9c — REQ-INST-14 (resource advertisement) + REQ-INST-15 (immutable home subnet + spt fork, ADR-0010): activate and build in D9, or document as forward seams and defer to M5? M4-PLAN left this open ('register/activate if in V1-mid line; else document').

Choices:
Both are feature bodies (registry projection + fork ceremony), not closeout work. doc-stage notes in CONTEXT/ADR; build M5. Keeps D9 a true closeout.
Home-subnet immutability touches creation paths already shipped — locking it now avoids later migration. INST-14 defers.
Full V1-mid line: build resource advertisement + home subnet/fork now. Grows D9 by roughly two slices.

Question: REQ-PAIR-7 (subnet icon, GUI-only consumer) — M4-PLAN said 'defer to D9 unless trivial'. It's inline image metadata on the subnet record; no GUI exists to consume it.

Choices:
Defer to M5 / GUI milestone (Recommended)
No consumer exists. Document as seam; required_stages stays [].
Add icon field to subnet record + replication now so wire format is stable before GUI work.


Your questions have been answered: "D9b two-host E2E — how to run it? The two CI runners (HFENDULEAM Windows + gravity Linux) ARE the rig. Cross-runner CI orchestration is possible but rendezvous between two GH Actions jobs over the real network is flaky territory. Alternative: env-gated int tests (e.g. SPT_TWO_HOST=1) + a scripted runbook run manually once on the rig, evidence captured with [int->REQ-*] tags on those tests."="Env-gated tests + manual runbook (Recommended)", "D9c — REQ-INST-14 (resource advertisement) + REQ-INST-15 (immutable home subnet + spt fork, ADR-0010): activate and build in D9, or document as forward seams and defer to M5? M4-PLAN left this open ('register/activate if in V1-mid line; else document')."="Activate both in D9", "REQ-PAIR-7 (subnet icon, GUI-only consumer) — M4-PLAN said 'defer to D9 unless trivial'. It's inline image metadata on the subnet record; no GUI exists to consume it."="Defer to M5 / GUI milestone (Recommended)". You can now continue with these answers in mind.

Question: D9b two-host E2E — how to run it? The two CI runners (HFENDULEAM Windows + gravity Linux) ARE the rig. Cross-runner CI orchestration is possible but rendezvous between two GH Actions jobs over the real network is flaky territory. Alternative: env-gated int tests (e.g. SPT_TWO_HOST=1) + a scripted runbook run manually once on the rig, evidence captured with [int->REQ-*] tags on those tests.

Choices:
Env-gated tests + manual runbook (Recommended)
Two-host int tests behind env gate, skipped in CI. Run once manually on rig; int evidence = real test runs. CI keeps loopback coverage. No flaky cross-runner choreography.
Workflow with paired jobs on both self-hosted runners that rendezvous over the network. Repeatable forever, but coordination + firewall/NAT flake risk in CI.
D9 ships env-gated tests + manual proof; a cross-runner CI job becomes a documented M5 seam.

Question: D9c — REQ-INST-14 (resource advertisement) + REQ-INST-15 (immutable home subnet + spt fork, ADR-0010): activate and build in D9, or document as forward seams and defer to M5? M4-PLAN left this open ('register/activate if in V1-mid line; else document').

Choices:
Both are feature bodies (registry projection + fork ceremony), not closeout work. doc-stage notes in CONTEXT/ADR; build M5. Keeps D9 a true closeout.
Home-subnet immutability touches creation paths already shipped — locking it now avoids later migration. INST-14 defers.
Full V1-mid line: build resource advertisement + home subnet/fork now. Grows D9 by roughly two slices.

Question: REQ-PAIR-7 (subnet icon, GUI-only consumer) — M4-PLAN said 'defer to D9 unless trivial'. It's inline image metadata on the subnet record; no GUI exists to consume it.

Choices:
Defer to M5 / GUI milestone (Recommended)
No consumer exists. Document as seam; required_stages stays [].
Add icon field to subnet record + replication now so wire format is stable before GUI work.

Env-gated tests + manual runbook (Recommended)

Defer to M5 / GUI milestone (Recommended)


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


214:**Shells differ structurally from agent endpoints** (full treatment below): a Shell has a node-local perch but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic**; and its lifecycle is link/teardown, not the dormant/suspended resting model of agent instances.
256:[Omitted long matching line]
270:- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;
271:- owner **suspended** → revive the owner, then online the shell;
334:[Omitted long matching line]
373:**dormant / suspended instance:**
375:- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
376:- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).
380:- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
381:- **dormant → suspended** — manual (`spt suspend` / `spt shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
384:A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).
386:**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override.
388:[Omitted long matching line]
395:**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).
397:**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)
402:- **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.
425: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:
431: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.


1	# Immutable home subnet; fork (copy-then-diverge) is the cross-subnet move, not re-home
2
3	## Status
4
5	accepted (2026-06-02)
6
7	## Context
8
9	ADR-0006 gave each endpoint a **home subnet** — the default sync scope (subnet-exclusive mind replication) and the anchor that qualifies its bare name — assigned at creation. It did not say whether home can *change* over an endpoint's life. The obvious feature request is "re-home": move an existing agent's mind from subnet A to subnet B.
10
11	Re-home is deceptively expensive. Under node-global identity (ADR-0003) with per-subnet registries and `hidden ⟹ not synced` (ADR-0006), moving the home of a *live* identity raises migration semantics with no clean answer: does the mind already replicated on A's nodes get purged or stranded? what happens to in-flight sync and the `a-<id>` branch on A? is the bare-name qualification anchor changing mid-life (breaking addresses others have cached)? Each is a real, separately-decidable hazard, and together they make re-home a migration subsystem, not a setting.
12
13	## Decision
14
15	**Home subnet is immutable. There is no re-home.** To place an agent's mind in a different subnet, **`spt fork`** it: create a **new, identity-distinct endpoint** whose home is the target subnet, **seeded with a one-time copy** of the source's mind (the live + project Cross-node Psyche sync tiers). The fork and source **diverge** immediately — no ongoing sync. The source is untouched; it may be deleted afterward (`--delete-source`).
16
17	This sidesteps every re-home hazard: nothing migrates, nothing is stranded, the source's mind and addresses are unchanged until the user explicitly deletes it. The fork's id defaults to the source's bare name if free in the target subnet (the ADR-0006 join-time collision check; else prompt/rename); its adapter follows the normal creation rule.
18
19	A fork is deliberately **not an instance** and **not a rename**:
20	- **instance** — *same* identity, another node, *synced* mind (ADR-0003).
21	- **rename** — *same* identity, new label, rippled (ADR-0006).
22	- **fork** — *new* identity, another subnet, *copied-then-independent* mind (this ADR).
23
24	## Consequences
25
26	- Endpoint identity gains a clean lifecycle — create → (rename | fork) → delete — with no scope-mutation operation. Code may assume an endpoint's home never changes.
27	- "Move my agent to the work fleet" is a copy + optional delete, not an in-place migration; the two endpoints are independent afterward (intentional — divergent minds in divergent fleets).
28	- `fork` is a new v1/M4 endpoint operation alongside `rename`; it reuses the join-time collision check and the adapter/creation rules. Cross-node *fork* (source and target on different nodes) composes with instantiate-anywhere's consent gate.
29	- Records the rejected alternative (re-home) so it is not re-proposed; extends ADR-0006 (multi-subnet membership) and builds on ADR-0003 (node-anchored identity).
30

# Immutable home subnet; fork (copy-then-diverge) is the cross-subnet move, not re-home

## Status

accepted (2026-06-02)

## Context

ADR-0006 gave each endpoint a **home subnet** — the default sync scope (subnet-exclusive mind replication) and the anchor that qualifies its bare name — assigned at creation. It did not say whether home can *change* over an endpoint's life. The obvious feature request is "re-home": move an existing agent's mind from subnet A to subnet B.

Re-home is deceptively expensive. Under node-global identity (ADR-0003) with per-subnet registries and `hidden ⟹ not synced` (ADR-0006), moving the home of a *live* identity raises migration semantics with no clean answer: does the mind already replicated on A's nodes get purged or stranded? what happens to in-flight sync and the `a-<id>` branch on A? is the bare-name qualification anchor changing mid-life (breaking addresses others have cached)? Each is a real, separately-decidable hazard, and together they make re-home a migration subsystem, not a setting.

## Decision

**Home subnet is immutable. There is no re-home.** To place an agent's mind in a different subnet, **`spt fork`** it: create a **new, identity-distinct endpoint** whose home is the target subnet, **seeded with a one-time copy** of the source's mind (the live + project Cross-node Psyche sync tiers). The fork and source **diverge** immediately — no ongoing sync. The source is untouched; it may be deleted afterward (`--delete-source`).

This sidesteps every re-home hazard: nothing migrates, nothing is stranded, the source's mind and addresses are unchanged until the user explicitly deletes it. The fork's id defaults to the source's bare name if free in the target subnet (the ADR-0006 join-time collision check; else prompt/rename); its adapter follows the normal creation rule.

A fork is deliberately **not an instance** and **not a rename**:
- **instance** — *same* identity, another node, *synced* mind (ADR-0003).
- **rename** — *same* identity, new label, rippled (ADR-0006).
- **fork** — *new* identity, another subnet, *copied-then-independent* mind (this ADR).

## Consequences

- Endpoint identity gains a clean lifecycle — create → (rename | fork) → delete — with no scope-mutation operation. Code may assume an endpoint's home never changes.
- "Move my agent to the work fleet" is a copy + optional delete, not an in-place migration; the two endpoints are independent afterward (intentional — divergent minds in divergent fleets).
- `fork` is a new v1/M4 endpoint operation alongside `rename`; it reuses the join-time collision check and the adapter/creation rules. Cross-node *fork* (source and target on different nodes) composes with instantiate-anywhere's consent gate.
- Records the rejected alternative (re-home) so it is not re-proposed; extends ADR-0006 (multi-subnet membership) and builds on ADR-0003 (node-anchored identity).


411-
412-**home subnet (per endpoint)**:
413-[Omitted long context line]
414-
415:**resource advertisement (subnet resource registry)**:
416:[Omitted long matching line]
417-
418-**resolution policy**:
419-[Omitted long context line]
420-
--
518-- **Outbound** from the endpoint → any **visible** node (the whitelist never restricts who it talks *to*).
519-- **Inbound** → a **reply** correlated to the endpoint's own prior outbound (`__REPLY_TO__`) is allowed from any visible node ("established/related"); an **unsolicited/direct** control or message ("new inbound") is allowed only from a **whitelisted** node.
520-- **Same-node operation is always allowed** (you're at the hardware; the home node never whitelists itself). The whitelist gates **remote (cross-node)** reach only.
521-
522:[Omitted long matching line]
523-
524-**Scope:** the gated *capabilities* (remote-exec, instantiate-anywhere) are deferred, so the *full* consent UX lands with them. **v1 ships the framework seam** — the grant-store shape + the deliver-consent-to-user mechanism — so the deferred capabilities drop into a coherent gate without a rewrite.
525-
526-### Messaging substrate


Found 1 file
crates\spt-store\src\visibility.rs


405
406	**subnet registry**:
407	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.
408
409	**endpoint visibility (per-(endpoint, subnet))**:
410	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.
411
412	**home subnet (per endpoint)**:
413	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).
414
415	**resource advertisement (subnet resource registry)**:
416	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).
417
418	**resolution policy**:
419	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).
420
421	**instantiate-anywhere** (seam day-one, behavior deferred):
422	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.
423
424	**cross-instance context freshness** (two-tier):
425	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:
426	- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
427	- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.
428
429	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.
430
431	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.
432
433	**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`.
434

**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`.


20-//! subnets on its **per-endpoint subnet-membership list** ([`sync_subnets`]
21-\! (VisibilityStore::sync_subnets)). An endpoint with **no list configured syncs
22-//! nowhere** — exclusive-by-default, fail-closed; the endpoint-creation flow seeds the
23://! list with the home subnet (ADR-0010: home is the default sync anchor), and D6's
24-\! replication consumes [`synced`](VisibilityStore::synced) as its gate.
25-//!
26-\! Node-local config under [`identity\`](crate::perch::identity_dir) — this is the


Grep

struct InstanceRow|struct RegistryRow|struct EndpointRecord|pub struct .*Registry


crates\spt-net\src\net\replicate.rs:40:pub struct RegistryUpdate {
crates\spt-net\src\net\registry.rs:78:pub struct SubnetRegistry {
crates\spt-daemon\src\seedmap.rs:68:pub struct SeedRegistry {


fn sync_subnets|fn home_subnet|seeds the list with the home|home


23://! list with the home subnet (ADR-0010: home is the default sync anchor), and D6's
53:    /// the creation flow seeds `[home]`).
141:    pub fn sync_subnets(&self, endpoint: &str) -> Option<&[String]> {
145:    /// Set the endpoint's sync-membership list (the creation flow seeds `[home]`).
174:        assert!(!vis.hidden("ling", "home", false), "both defaults off => visible");
175:        assert!(vis.hidden("ling", "home", true), "subnet hide_new_endpoints hides");
178:        assert!(vis.hidden("ling", "home", true), "both on still hides");
187:        vis.set_override("ling", "home", Some(false));
188:        assert!(!vis.hidden("ling", "home", true), "visible-override beats OR-true");
190:        vis.set_override("other", "home", Some(true));
191:        assert!(vis.hidden("other", "home", false), "hidden-override beats OR-false");
194:        vis.set_override("ling", "home", None);
195:        assert!(vis.hidden("ling", "home", true), "cleared override => defaults rule");
207:        vis.set_override("ling", "home", Some(false));
208:        vis.set_sync_subnets("ling", vec!["home".to_string()]);
223:    // creation flow seeds [home]).
227:        assert!(!vis.synced("ling", "home", false), "no list configured => nowhere");
228:        vis.set_sync_subnets("ling", vec!["home".to_string()]);
229:        assert!(vis.synced("ling", "home", false), "listed + visible => synced");
238:        vis.set_sync_subnets("ling", vec!["home".to_string(), "work".to_string()]);

[Showing results with pagination = limit: 20]