1	# TOTP-seeded SPAKE2 node pairing
2
3	## Status
4
5	accepted (2026-05-29)
6
7	## Context
8
9	"Zero-config WAN that just works" still requires a first-contact ceremony by which a user's new node is authorized into their existing subnet (key exchange + mutual trust), with no account and no central operator. The research brief vetted Ed25519 identity, Noise, Magic-Wormhole-style PAKE, and TOFU. The product question is the day-one pairing UX for a single user's own machines.
10
11	A natural-seeming option is TOTP: the user provisions a seed into an authenticator app and pairs future nodes with the rotating 6-digit code. Used the obvious way (a new node sends the code to a trusted node for verification), this is insecure: the code is a ~20-bit bearer token, replayable within its 30s window, not bound to the key being exchanged — a real-time MITM can replay the code with their own pubkey and join the subnet. It also performs no key exchange, requiring a separate unbound handshake.
12
13	## Decision
14
15	Adopt **TOTP-seeded SPAKE2 pairing**: keep the familiar TOTP UX but use the 6-digit code as the **password for a SPAKE2 (PAKE) handshake**, not as a verified bearer token.
16
17	- The TOTP **seed** is the durable subnet secret: generated on the first node, shown as a QR for the user's authenticator app, and replicated to every already-trusted node.
18	- To pair a new node, a trusted node (online, no human — it holds the seed) computes the current code as its SPAKE2 password; the user reads the code off their phone and types it into the new node as its password. Matching codes → PAKE succeeds → pubkeys exchanged + bound, MITM-resistant encrypted channel.
19
20	PAKE is the correct primitive for a low-entropy human-transcribed secret: it resists offline brute force (one online guess per attempt) and binds the code to the established key. Cryptographic strength comes from the PAKE construction, not from the 6 digits being secret.
21
22	Rejected alternatives:
23	- **TOTP-as-bearer-token** — replayable, unbound, MITM-vulnerable (above).
24	- **Plain Magic Wormhole / fresh-code-each-time** — secure, but needs a human/relay at *both* ends each pairing. TOTP-seeded PAKE keeps the persistent seed on online trusted nodes so the user interacts only with the new node.
25	- **Paste-the-key** — too error-prone for "just works."
26	- **Account-anchored auto-trust** — reintroduces the account dependency the product is shedding.
27
28	Trust model: local per-node trust store of authorized peer pubkeys; TOFU after pairing (accept on first post-ceremony connect, warn on key change). Cross-subnet (two different users) is out of scope for v1.
29
30	## Consequences
31
32	- A seed-holder node must be online and reachable by the untrusted new node at pairing time; the relay/discovery layer must permit pre-trust contact on a dedicated pairing ALPN.
33	- Operational details to implement: ±1 TOTP window tolerance, handshake-attempt rate limiting, QR re-provisioning for recovery from any still-trusted node.
34	- Losing all nodes *and* the authenticator seed makes the subnet unrecoverable — must be documented.
35	- Revocation of a paired node is a separate trust-store delete; TOTP gates joining only, not continued membership.
36	- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).
37	- Because every paired node holds the seed, the current code is fetchable from any trusted node (`spt pair --code` or via an agent) — no phone needed if a trusted node is handy. The user is prompted to **name the subnet** on first pairing; the name is shown on every subsequent pairing for confidence as the fleet grows.
38
39	## Stage A red-team amendments (2026-05-31)
40
41	Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:
42
43	- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.
44	- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.
45	- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).
46
47	## Multi-subnet amendment (2026-06-01 — see ADR-0006)
48
49	ADR-0006 makes a node a member of *multiple* subnets. The pairing ceremony is refined (not reversed) accordingly:
50
51	- **Discovery takes two inputs: TOTP code + subnet-name.** The code is also the relay rendezvous selector, but a 6-digit code is a weak global selector, so the subnet-name namespaces the rendezvous. This is the **unified rule for all cases** — there is no node-name mode.
52	- **"Create-new subnet" names the subnet at creation.** One machine generates the new seed *and* runs the R-PAIR-4 naming prompt up front (becoming the sole seed-holder); the joiner then uses that subnet-name + the new seed's code. Naming simply moves to link-start.
53	- **Rendezvous-token hashing.** The relay routes the pre-trust ceremony by `H(subnet-name ‖ TOTP-epoch)`, not the plaintext label — the user/agent enters the raw name + code; the hash is internal. (Payload is already E2E-encrypted; this protects the relay-visible routing token from leaking the human subnet label.) Composes with the #12 transcript binding (the subnet ID / seed epoch already in the transcript).
54	- **Per-subnet code fetch is gated behind OS elevation** (Windows UAC / Linux root-or-equivalent), or an elevated agent endpoint; else the user falls back to their authenticator app. Replaces the old unqualified "fetchable from any trusted node" with an elevation-gated, *per-subnet* fetch (a node now holds several subnets' seeds).
55	- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.
56	- **Forward (cross-user, ADR-0006 seam):** the seed key becomes per-(subnet, user); the prompt becomes 2-stage. The #10 seed epoch+rotation and #11 subnet-global rate-limit requirements apply **per subnet**.
57

# TOTP-seeded SPAKE2 node pairing

## Status

accepted (2026-05-29)

## Context

"Zero-config WAN that just works" still requires a first-contact ceremony by which a user's new node is authorized into their existing subnet (key exchange + mutual trust), with no account and no central operator. The research brief vetted Ed25519 identity, Noise, Magic-Wormhole-style PAKE, and TOFU. The product question is the day-one pairing UX for a single user's own machines.

A natural-seeming option is TOTP: the user provisions a seed into an authenticator app and pairs future nodes with the rotating 6-digit code. Used the obvious way (a new node sends the code to a trusted node for verification), this is insecure: the code is a ~20-bit bearer token, replayable within its 30s window, not bound to the key being exchanged — a real-time MITM can replay the code with their own pubkey and join the subnet. It also performs no key exchange, requiring a separate unbound handshake.

## Decision

Adopt **TOTP-seeded SPAKE2 pairing**: keep the familiar TOTP UX but use the 6-digit code as the **password for a SPAKE2 (PAKE) handshake**, not as a verified bearer token.

- The TOTP **seed** is the durable subnet secret: generated on the first node, shown as a QR for the user's authenticator app, and replicated to every already-trusted node.
- To pair a new node, a trusted node (online, no human — it holds the seed) computes the current code as its SPAKE2 password; the user reads the code off their phone and types it into the new node as its password. Matching codes → PAKE succeeds → pubkeys exchanged + bound, MITM-resistant encrypted channel.

PAKE is the correct primitive for a low-entropy human-transcribed secret: it resists offline brute force (one online guess per attempt) and binds the code to the established key. Cryptographic strength comes from the PAKE construction, not from the 6 digits being secret.

Rejected alternatives:
- **TOTP-as-bearer-token** — replayable, unbound, MITM-vulnerable (above).
- **Plain Magic Wormhole / fresh-code-each-time** — secure, but needs a human/relay at *both* ends each pairing. TOTP-seeded PAKE keeps the persistent seed on online trusted nodes so the user interacts only with the new node.
- **Paste-the-key** — too error-prone for "just works."
- **Account-anchored auto-trust** — reintroduces the account dependency the product is shedding.

Trust model: local per-node trust store of authorized peer pubkeys; TOFU after pairing (accept on first post-ceremony connect, warn on key change). Cross-subnet (two different users) is out of scope for v1.

## Consequences

- A seed-holder node must be online and reachable by the untrusted new node at pairing time; the relay/discovery layer must permit pre-trust contact on a dedicated pairing ALPN.
- Operational details to implement: ±1 TOTP window tolerance, handshake-attempt rate limiting, QR re-provisioning for recovery from any still-trusted node.
- Losing all nodes *and* the authenticator seed makes the subnet unrecoverable — must be documented.
- Revocation of a paired node is a separate trust-store delete; TOTP gates joining only, not continued membership.
- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).
- Because every paired node holds the seed, the current code is fetchable from any trusted node (`spt pair --code` or via an agent) — no phone needed if a trusted node is handy. The user is prompted to **name the subnet** on first pairing; the name is shown on every subsequent pairing for confidence as the fleet grows.

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:

- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.
- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.
- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).

## Multi-subnet amendment (2026-06-01 — see ADR-0006)

ADR-0006 makes a node a member of *multiple* subnets. The pairing ceremony is refined (not reversed) accordingly:

- **Discovery takes two inputs: TOTP code + subnet-name.** The code is also the relay rendezvous selector, but a 6-digit code is a weak global selector, so the subnet-name namespaces the rendezvous. This is the **unified rule for all cases** — there is no node-name mode.
- **"Create-new subnet" names the subnet at creation.** One machine generates the new seed *and* runs the R-PAIR-4 naming prompt up front (becoming the sole seed-holder); the joiner then uses that subnet-name + the new seed's code. Naming simply moves to link-start.
- **Rendezvous-token hashing.** The relay routes the pre-trust ceremony by `H(subnet-name ‖ TOTP-epoch)`, not the plaintext label — the user/agent enters the raw name + code; the hash is internal. (Payload is already E2E-encrypted; this protects the relay-visible routing token from leaking the human subnet label.) Composes with the #12 transcript binding (the subnet ID / seed epoch already in the transcript).
- **Per-subnet code fetch is gated behind OS elevation** (Windows UAC / Linux root-or-equivalent), or an elevated agent endpoint; else the user falls back to their authenticator app. Replaces the old unqualified "fetchable from any trusted node" with an elevation-gated, *per-subnet* fetch (a node now holds several subnets' seeds).
- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.
- **Forward (cross-user, ADR-0006 seam):** the seed key becomes per-(subnet, user); the prompt becomes 2-stage. The #10 seed epoch+rotation and #11 subnet-global rate-limit requirements apply **per subnet**.


NOTE FROM THEUSER: proceed with nextsteps.


Dense + binding (pairing red-team FATAL #10, SERIOUS #11/#12). Reading remaining ADRs (0003 instances, 0006 subnet, 0007 notif) + workspace layout for D0 scaffold pattern.


1	# Multi-instance, node-anchored identity model
2
3	## Status
4
5	accepted (2026-05-29) · amended (2026-05-30) — see Amendment below; the original framing over-weighted remote-drive as the *definition* of multi-instance.
6
7	## Amendment (2026-05-30): native-instances-with-synced-mind is primary; remote-drive is a separate mode
8
9	The original decision below framed "operate a LiveAgent on any node from any other machine" primarily as **remote PTY drive**. That is backwards-weighted. Corrected model:
10
11	- **Primary — instance:** the *same* endpoint (same harness+adapter) running **natively** on a machine (local files, local compute), with the **Psyche mind synced** across instances. Files/session-history are per-node and cannot teleport; the mind follows. This is the core meaning of "same agent on multiple machines."
12	- **Instance dormancy:** typically one instance is actively-driven; others are **dormant** (live, state-preserved, re-activatable in place). Switching machines makes the previously-driven instance dormant — no explicit stop. Registry status is active / dormant / offline.
13	- **Remote-control (separate, Shell-like):** attaching a control/view surface to an instance *running on another node* (compute+files stay remote; you are a viewport — the byte-stream terminal attach). A driven surface, user→agent direction; distinct from the instance concept.
14	- **Handoff (remote-control → local):** `git pull` brings files; the fresh-with-preload resume seam preloads the other instance's synced context; the remote instance goes dormant. No teardown.
15	- **Two-tier context sync:** **live context** (per-agent) syncs to all instances; **project context** (per-agent-per-project) syncs only to same-project instances. Resolves divergent simultaneous work without losing "mind follows everywhere." (Depends on refining the Psyche authoring directives so each tier holds the right material.)
16
17	The decision below stands except where it implies remote-drive is the primary realization of multi-instance — read "drivable from anywhere" as "runnable natively on any of its nodes with synced mind, **plus** an optional remote-control surface mode."
18
19	## Context
20
21	The sister project models an agent as essentially one perch on one machine, and explicitly deferred "agent name resolution" across a subnet. spt-core commits to two user-facing requirements that break that model: (a) the user can interact with any endpoint from any machine, and (b) a LiveAgent can be operated on any node it has ever been instantiated on, from any other machine. The Instances concept (each endpoint classifies as an instance; the same endpoint ID can have instances across multiple machines with equal functionality) requires deciding what an "instance" actually is before any of it can be built.
22
23	## Decision
24
25	Split agent identity into two layers: a subnet-wide **endpoint ID** (the logical identity, `ling`) and an **instance** (a materialization of that ID on a specific node, `ling@desktop`).
26
27	**Instances are node-anchored, not portable.** An instance's substance — project working directory, harness session history, Psyche context, spool — lives on its node's filesystem and does not teleport. Instances of one ID are distinct *seats* (possibly on different projects), sharing only identity. "Equal functionality across all machines" therefore means *any instance is drivable with the full command surface from any node*, NOT that instances share state.
28
29	Rejected alternative — **portable/synced instances** (one logical conversation+state replicated across nodes): a LiveAgent's whole point is operating inside a project on a machine; the project filesystem can't be teleported, and full state replication across nodes is a distributed-consistency problem with no payoff for the actual use case (drive my desktop agent from my laptop).
30
31	Remote drive is realized by daemon-to-daemon PTY proxying over the network session-surface abstraction — enabled by the prior decision (ADR-0002) to collapse PTY-host and network-host into one `spt-daemon`.
32
33	**v1 scope (V1-mid):** ship the data-model split, the subnet registry (the now-mandatory name-resolution layer), the resolution policy, remote-drive of already-running instances, and cross-node Psyche sync transport. Defer (seam-compatible) instantiate-anywhere + its consent gate, remote command execution + its security gate, and the cross-instance context-freshness auto-feed rule.
34
35	## Consequences
36
37	- The subnet registry becomes a mandatory, first-class subsystem (eventually-consistent `endpoint_id → [instances]`), not a deferred nicety.
38	- Bare-ID addressing needs a deterministic resolution policy (local → most-recently-active → explicit `id@node` override).
39	- The networking decision (ADR-0002) is now load-bearing for a flagship user feature, not just messaging — remote drive depends on it working well.
40	- Cross-node Psyche sync over P2P replaces the sister project's GitHub-repo sync, removing the `gh`/account/setup dependency.
41	- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.
42
43	## Stage A red-team amendments (2026-05-31)
44
45	Codex (`docs/reviews/STAGE-A-codex-redteam.md`) flagged three SERIOUS issues; resolve before M4:
46
47	- **#7 — wall-clock "newest" loses concurrent context writes.** Two active instances editing the same live topic is a semantic *conflict*, not a stale-write race; wall-clock newest silently drops a valid update, and LLM-authored live/project tiering is too soft to be the conflict boundary. **Required:** monotonic per-node epochs / vector clocks (or CRDT-ish merge / explicit conflict review) for the cross-node Psyche merge — the distributed generalization of KNOWN-HAZARDS 6.5. *Decision pending: which merge model.*
48	- **#8 — `local → most-recently-active → id@node` is ambiguous under partition/clock-skew.** Can route messages or remote-control to the wrong instance around active/dormant transitions. **Required:** registry lease/epoch using monotonic per-node epochs, not wall-clock; chaos-test stale entries + ambiguous active instances.
49	- **#9 — dormant-warm default has real resource/security cost.** Warm sessions retain credentials, file locks, model/billing footprint, PTYs, watchers on *every* node. **Required:** measure N dormant sessions on real adapters; define an adapter-specific warm/cold policy + resource budget before locking warm as the default (R-INST-3).
50

# Multi-instance, node-anchored identity model

## Status

accepted (2026-05-29) · amended (2026-05-30) — see Amendment below; the original framing over-weighted remote-drive as the *definition* of multi-instance.

## Amendment (2026-05-30): native-instances-with-synced-mind is primary; remote-drive is a separate mode

The original decision below framed "operate a LiveAgent on any node from any other machine" primarily as **remote PTY drive**. That is backwards-weighted. Corrected model:

- **Primary — instance:** the *same* endpoint (same harness+adapter) running **natively** on a machine (local files, local compute), with the **Psyche mind synced** across instances. Files/session-history are per-node and cannot teleport; the mind follows. This is the core meaning of "same agent on multiple machines."
- **Instance dormancy:** typically one instance is actively-driven; others are **dormant** (live, state-preserved, re-activatable in place). Switching machines makes the previously-driven instance dormant — no explicit stop. Registry status is active / dormant / offline.
- **Remote-control (separate, Shell-like):** attaching a control/view surface to an instance *running on another node* (compute+files stay remote; you are a viewport — the byte-stream terminal attach). A driven surface, user→agent direction; distinct from the instance concept.
- **Handoff (remote-control → local):** `git pull` brings files; the fresh-with-preload resume seam preloads the other instance's synced context; the remote instance goes dormant. No teardown.
- **Two-tier context sync:** **live context** (per-agent) syncs to all instances; **project context** (per-agent-per-project) syncs only to same-project instances. Resolves divergent simultaneous work without losing "mind follows everywhere." (Depends on refining the Psyche authoring directives so each tier holds the right material.)

The decision below stands except where it implies remote-drive is the primary realization of multi-instance — read "drivable from anywhere" as "runnable natively on any of its nodes with synced mind, **plus** an optional remote-control surface mode."

## Context

The sister project models an agent as essentially one perch on one machine, and explicitly deferred "agent name resolution" across a subnet. spt-core commits to two user-facing requirements that break that model: (a) the user can interact with any endpoint from any machine, and (b) a LiveAgent can be operated on any node it has ever been instantiated on, from any other machine. The Instances concept (each endpoint classifies as an instance; the same endpoint ID can have instances across multiple machines with equal functionality) requires deciding what an "instance" actually is before any of it can be built.

## Decision

Split agent identity into two layers: a subnet-wide **endpoint ID** (the logical identity, `ling`) and an **instance** (a materialization of that ID on a specific node, `ling@desktop`).

**Instances are node-anchored, not portable.** An instance's substance — project working directory, harness session history, Psyche context, spool — lives on its node's filesystem and does not teleport. Instances of one ID are distinct *seats* (possibly on different projects), sharing only identity. "Equal functionality across all machines" therefore means *any instance is drivable with the full command surface from any node*, NOT that instances share state.

Rejected alternative — **portable/synced instances** (one logical conversation+state replicated across nodes): a LiveAgent's whole point is operating inside a project on a machine; the project filesystem can't be teleported, and full state replication across nodes is a distributed-consistency problem with no payoff for the actual use case (drive my desktop agent from my laptop).

Remote drive is realized by daemon-to-daemon PTY proxying over the network session-surface abstraction — enabled by the prior decision (ADR-0002) to collapse PTY-host and network-host into one `spt-daemon`.

**v1 scope (V1-mid):** ship the data-model split, the subnet registry (the now-mandatory name-resolution layer), the resolution policy, remote-drive of already-running instances, and cross-node Psyche sync transport. Defer (seam-compatible) instantiate-anywhere + its consent gate, remote command execution + its security gate, and the cross-instance context-freshness auto-feed rule.

## Consequences

- The subnet registry becomes a mandatory, first-class subsystem (eventually-consistent `endpoint_id → [instances]`), not a deferred nicety.
- Bare-ID addressing needs a deterministic resolution policy (local → most-recently-active → explicit `id@node` override).
- The networking decision (ADR-0002) is now load-bearing for a flagship user feature, not just messaging — remote drive depends on it working well.
- Cross-node Psyche sync over P2P replaces the sister project's GitHub-repo sync, removing the `gh`/account/setup dependency.
- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) flagged three SERIOUS issues; resolve before M4:

- **#7 — wall-clock "newest" loses concurrent context writes.** Two active instances editing the same live topic is a semantic *conflict*, not a stale-write race; wall-clock newest silently drops a valid update, and LLM-authored live/project tiering is too soft to be the conflict boundary. **Required:** monotonic per-node epochs / vector clocks (or CRDT-ish merge / explicit conflict review) for the cross-node Psyche merge — the distributed generalization of KNOWN-HAZARDS 6.5. *Decision pending: which merge model.*
- **#8 — `local → most-recently-active → id@node` is ambiguous under partition/clock-skew.** Can route messages or remote-control to the wrong instance around active/dormant transitions. **Required:** registry lease/epoch using monotonic per-node epochs, not wall-clock; chaos-test stale entries + ambiguous active instances.
- **#9 — dormant-warm default has real resource/security cost.** Warm sessions retain credentials, file locks, model/billing footprint, PTYs, watchers on *every* node. **Required:** measure N dormant sessions on real adapters; define an adapter-specific warm/cold policy + resource budget before locking warm as the default (R-INST-3).


1	# Multi-subnet membership model
2
3	## Status
4
5	accepted (2026-06-01)
6
7	## Context
8
9	ADR-0003 anchored identity to the node; ADR-0005 made a subnet = one TOTP seed = one user's mutually-trusted nodes, with cross-*user* subnets explicitly deferred. Both were written under an unstated assumption: **one subnet per node.** New feature work (node link UX, "join existing vs create new subnet", per-subnet endpoint hiding, subnet event notifications) requires a node to belong to **multiple subnets at once**, and the "external subnet pairing request" notion presupposes that multiplicity already exists. This forces the one-subnet assumption into the open and asks how identity, addressing, the registry, sync, pairing, and visibility behave when a node is a member of several subnets.
10
11	The constraint: satisfy multi-subnet membership now **without** un-deferring cross-*user* subnets (a much larger stranger-authentication redesign), while shaping the model so cross-user can drop in later.
12
13	## Decision
14
15	**A node may be a member of multiple subnets simultaneously — for v1, only multiple subnets of the *same user*** (e.g. a `home` fleet and a `work` fleet). A node holds *N* TOTP seeds / trust-contexts instead of one. The per-subnet trust boundary is unchanged (within each subnet, all nodes are the one user's own). Cross-*user* membership stays deferred but the model is built multiplicity-ready.
16
17	Resolved sub-decisions:
18
19	1. **Node-global identity, per-subnet visibility.** One `ling` exists on a node (one mind, one `tracked/` context); the node advertises it into each subnet it belongs to, subject to visibility (below). The subnet registry is **per-subnet**; the same endpoint can appear in several registries. Bare-id uniqueness holds *within* a subnet via a **join-time collision check**.
20
21	2. **Qualified addressing + rippled rename.** Two combinable qualifiers — subnet-qualified `home:ling` and node-qualified `ling@hfenduleam`. When a bare id is ambiguous across visible subnets (two distinct endpoints sharing a name), resolution **refuses and forces qualification** rather than guessing. A new **`spt rename <id> <new_id>`** changes an endpoint's logical ID and ripples to all instances (registry, every perch, the `a-<id>` context branch), collision-checked against every target subnet and reconciled by the 6.5 precedence marker.
22
23	3. **Unified subnet-name link discovery.** The TOTP code is also the relay rendezvous selector; discovery+link takes **TOTP code + subnet-name** in *all* cases. "Create-new" **names the subnet at creation** (no node-name mode). The relay rendezvous token is `H(subnet-name ‖ TOTP-epoch)` (under the hood; the user enters the raw name + code) so the plaintext label isn't exposed on the public relay. Payload is E2E-encrypted regardless (R-NET-2). See the ADR-0005 amendment.
24
25	4. **Per-subnet code fetch gated behind OS elevation.** Retrieving a subnet's current code *from a node* requires Windows UAC / Linux root-or-equivalent, or an elevated agent endpoint; otherwise the user falls back to their own authenticator app. Narrows the multi-subnet exposure (mere CLI/agent presence no longer yields any subnet's join-code).
26
27	5. **Subnet-exclusive sync.** An endpoint's mind replicates only within its home subnet by default. Endpoint config carries a **subnet-membership list** for sync; the mind replicates across every listed subnet's nodes. Distinct from visibility: being addressable in a subnet does not imply replicating the mind there.
28
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`.
30
31	**Cross-user seam (forward, not built):** the seed key generalizes from per-subnet to **per-(subnet, user)**; the pairing/code prompt becomes 2-stage (user → subnet), collapsing to 1-stage under v1's single implicit user. This yields node↔user attribution and single-operation force-unlink of a user's whole node-group for free. v1 stores seeds in a shape that already accommodates the (subnet, user) key.
32
33	## Consequences
34
35	- The subnet registry, pairing, code-fetch, and context-sync all gain a subnet dimension; "one user's nodes" generalizes to "one user's nodes per subnet."
36	- New v1 requirements to register: multi-subnet membership, qualified addressing, `rename`, endpoint visibility, elevation-gated code fetch. These land in M4 (networking + registry + pairing).
37	- Endpoint visibility introduces a per-`(endpoint, subnet)` ACL surface the registry must carry and the resolution/routing path must enforce (excluded = unroutable).
38	- The cross-user (b) future is now a **seam, not a rewrite**: seed key, 2-stage prompt, `from`-attributed notifs, and excluded-visibility boundaries are all shaped for it.
39	- Amends ADR-0005 (pairing ceremony) — see that ADR's amendment; builds on ADR-0003 (node-anchored identity).
40

# Multi-subnet membership model

## Status

accepted (2026-06-01)

## Context

ADR-0003 anchored identity to the node; ADR-0005 made a subnet = one TOTP seed = one user's mutually-trusted nodes, with cross-*user* subnets explicitly deferred. Both were written under an unstated assumption: **one subnet per node.** New feature work (node link UX, "join existing vs create new subnet", per-subnet endpoint hiding, subnet event notifications) requires a node to belong to **multiple subnets at once**, and the "external subnet pairing request" notion presupposes that multiplicity already exists. This forces the one-subnet assumption into the open and asks how identity, addressing, the registry, sync, pairing, and visibility behave when a node is a member of several subnets.

The constraint: satisfy multi-subnet membership now **without** un-deferring cross-*user* subnets (a much larger stranger-authentication redesign), while shaping the model so cross-user can drop in later.

## Decision

**A node may be a member of multiple subnets simultaneously — for v1, only multiple subnets of the *same user*** (e.g. a `home` fleet and a `work` fleet). A node holds *N* TOTP seeds / trust-contexts instead of one. The per-subnet trust boundary is unchanged (within each subnet, all nodes are the one user's own). Cross-*user* membership stays deferred but the model is built multiplicity-ready.

Resolved sub-decisions:

1. **Node-global identity, per-subnet visibility.** One `ling` exists on a node (one mind, one `tracked/` context); the node advertises it into each subnet it belongs to, subject to visibility (below). The subnet registry is **per-subnet**; the same endpoint can appear in several registries. Bare-id uniqueness holds *within* a subnet via a **join-time collision check**.

2. **Qualified addressing + rippled rename.** Two combinable qualifiers — subnet-qualified `home:ling` and node-qualified `ling@hfenduleam`. When a bare id is ambiguous across visible subnets (two distinct endpoints sharing a name), resolution **refuses and forces qualification** rather than guessing. A new **`spt rename <id> <new_id>`** changes an endpoint's logical ID and ripples to all instances (registry, every perch, the `a-<id>` context branch), collision-checked against every target subnet and reconciled by the 6.5 precedence marker.

3. **Unified subnet-name link discovery.** The TOTP code is also the relay rendezvous selector; discovery+link takes **TOTP code + subnet-name** in *all* cases. "Create-new" **names the subnet at creation** (no node-name mode). The relay rendezvous token is `H(subnet-name ‖ TOTP-epoch)` (under the hood; the user enters the raw name + code) so the plaintext label isn't exposed on the public relay. Payload is E2E-encrypted regardless (R-NET-2). See the ADR-0005 amendment.

4. **Per-subnet code fetch gated behind OS elevation.** Retrieving a subnet's current code *from a node* requires Windows UAC / Linux root-or-equivalent, or an elevated agent endpoint; otherwise the user falls back to their own authenticator app. Narrows the multi-subnet exposure (mere CLI/agent presence no longer yields any subnet's join-code).

5. **Subnet-exclusive sync.** An endpoint's mind replicates only within its home subnet by default. Endpoint config carries a **subnet-membership list** for sync; the mind replicates across every listed subnet's nodes. Distinct from visibility: being addressable in a subnet does not imply replicating the mind there.

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

**Cross-user seam (forward, not built):** the seed key generalizes from per-subnet to **per-(subnet, user)**; the pairing/code prompt becomes 2-stage (user → subnet), collapsing to 1-stage under v1's single implicit user. This yields node↔user attribution and single-operation force-unlink of a user's whole node-group for free. v1 stores seeds in a shape that already accommodates the (subnet, user) key.

## Consequences

- The subnet registry, pairing, code-fetch, and context-sync all gain a subnet dimension; "one user's nodes" generalizes to "one user's nodes per subnet."
- New v1 requirements to register: multi-subnet membership, qualified addressing, `rename`, endpoint visibility, elevation-gated code fetch. These land in M4 (networking + registry + pairing).
- Endpoint visibility introduces a per-`(endpoint, subnet)` ACL surface the registry must carry and the resolution/routing path must enforce (excluded = unroutable).
- The cross-user (b) future is now a **seam, not a rewrite**: seed key, 2-stage prompt, `from`-attributed notifs, and excluded-visibility boundaries are all shaped for it.
- Amends ADR-0005 (pairing ceremony) — see that ADR's amendment; builds on ADR-0003 (node-anchored identity).


1	# Subnet notification primitive
2
3	## Status
4
5	accepted (2026-06-01)
6
7	## Context
8
9	Three flows independently needed "deliver something to the user's most-recently-active session and make sure they see it": the **self-update prompt** (CONTEXT §Self-update), the **consent escalation** (CONTEXT §Consent & security gates — both already described as "the registry-resolution precursor to PresenceChannel"), and emerging **subnet events** (node paired, external-subnet pairing request). Each was about to grow its own ad-hoc delivery + "did the user see it?" handling. They are the same thing.
10
11	## Decision
12
13	Introduce a **notification (notif)** as a first-class kind, distinct from inter-agent messages, and **refactor the self-update prompt and consent escalation to be notif producers** (one primitive, open producer set). A notif is **user-directed, dismissable, and resurfacing**; a message is agent→agent and consumed once.
14
15	- **Per-subnet, replicated spool.** Notifs are subnet events; the spool replicates across that subnet's nodes (reusing subnet-registry distribution, ADR-0006). Each notif is subnet-tagged; **dismiss-state replicates subnet-wide**, eventually consistent.
16	- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.
17	- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).
18	- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalization are one.
19	- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).
20
21	## Consequences
22
23	- Update-prompt and consent-escalation delivery code collapses onto one primitive; the deferred PresenceChannel later generalizes this primitive's delivery step rather than three separate ones.
24	- New v1 requirement to register: the notification primitive + `spt notify` + the `notif_command` manifest seam. Lands with M4 (subnet-distributed spool needs the registry-distribution layer); the manifest seam is authored alongside.
25	- **Forward (cross-user, ADR-0006 seam):** dismissal and the spool key generalize to per-(subnet, user); `spt notify` gains targeting (all subnet users by default, or specific users); the `from` field is what makes attributed/targeted delivery possible.
26	- Builds on ADR-0006 (subnet distribution, visibility scoping) and the existing presence-resolution + inject-input machinery.
27

# Subnet notification primitive

## Status

accepted (2026-06-01)

## Context

Three flows independently needed "deliver something to the user's most-recently-active session and make sure they see it": the **self-update prompt** (CONTEXT §Self-update), the **consent escalation** (CONTEXT §Consent & security gates — both already described as "the registry-resolution precursor to PresenceChannel"), and emerging **subnet events** (node paired, external-subnet pairing request). Each was about to grow its own ad-hoc delivery + "did the user see it?" handling. They are the same thing.

## Decision

Introduce a **notification (notif)** as a first-class kind, distinct from inter-agent messages, and **refactor the self-update prompt and consent escalation to be notif producers** (one primitive, open producer set). A notif is **user-directed, dismissable, and resurfacing**; a message is agent→agent and consumed once.

- **Per-subnet, replicated spool.** Notifs are subnet events; the spool replicates across that subnet's nodes (reusing subnet-registry distribution, ADR-0006). Each notif is subnet-tagged; **dismiss-state replicates subnet-wide**, eventually consistent.
- **Two states:** *seen* (surfaced at least once, tracked per-endpoint) vs *dismissed* (explicit ack — `spt notif dismiss`, or the agent marks it). Surfacing ≠ dismissal.
- **First-fire** → the user's most-recently-active endpoint in that subnet (presence resolution). **Resurface** undismissed notifs at existing reported boundaries — state→active (`wake`), `api boundary clear`, `api boundary compact`, new-session-start — gated by *seen-per-endpoint* plus a **cross-endpoint suppression timeout** (default ~1h) so a notif can't bounce between endpoints. Scoped to subnets the endpoint is **visible in** (ADR-0006).
- **Delivery reuses messaging.** A notif to an agent is a notif-flavored envelope delivered to the agent's perch via the existing path; the agent surfaces it. An optional **`notif_command` manifest template** (on **both** harness-adapter and shell-adapter manifests) renders it endpoint-native (OS toast, GameRobot `alert-symbol`), combinable with agent-surface. If presence resolves to a **Shell** attached to the endpoint, the notif renders via *that shell's* template — the v1 seam and the M5 generalization are one.
- **`spt notify`** lets any agent issue a subnet-wide notif (v1: reaches the user on their active endpoint). The **envelope `from`** carries the issuer's endpoint id + node + subnet (subnet surfaced only to multi-subnet receivers).

## Consequences

- Update-prompt and consent-escalation delivery code collapses onto one primitive; the deferred PresenceChannel later generalizes this primitive's delivery step rather than three separate ones.
- New v1 requirement to register: the notification primitive + `spt notify` + the `notif_command` manifest seam. Lands with M4 (subnet-distributed spool needs the registry-distribution layer); the manifest seam is authored alongside.
- **Forward (cross-user, ADR-0006 seam):** dismissal and the spool key generalize to per-(subnet, user); `spt notify` gains targeting (all subnet users by default, or specific users); the `from` field is what makes attributed/targeted delivery possible.
- Builds on ADR-0006 (subnet distribution, visibility scoping) and the existing presence-resolution + inject-input machinery.


Architecture absorbed. Red-team amendments are binding (pairing #10/#11/#12; instances #7/#8/#9). M4 large but D0-D8 frame holds — needs multi-subnet dimension + red-team reqs folded in. Examining workspace + an existing sibling crate (spt-term) for D0 scaffold pattern + traceable-reqs registry.


Bash

echo "===ROOT CARGO==="; cat Cargo.toml 2>/dev/null; echo "===CRATES==="; ls crates/ 2>/dev/null; echo "===SPT-TERM CARGO==="; cat crates/spt-term/Cargo.toml 2>/dev/null; echo "===SPT-TERM SRC==="; ls crates/spt-term/src/ 2>/dev/null

Inspect workspace + spt-term scaffold


Command running in background with ID: bs1xjrp90. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\bs1xjrp90.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bs1xjrp90</task-id>
<tool-use-id>toolu_013YqhCtzqy9oSxE7HChh7wm</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\bs1xjrp90.output</output-file>
<status>completed</status>
<summary>Background command "Inspect workspace + spt-term scaffold" completed (exit code 0)</summary>
</task-notification>


1	===ROOT CARGO===
2	# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
3	#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
4	# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
5	# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
6	# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
7	# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
8	# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
9	# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
10	[workspace]
11	resolver = "2"
12	members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock"]
13	===CRATES===
14	spt
15	spt-daemon
16	spt-live
17	spt-msg
18	spt-proto
19	spt-runtime
20	spt-store
21	spt-term
22	===SPT-TERM CARGO===
23	[package]
24	name = "spt-term"
25	version = "0.0.0"
26	edition = "2021"
27	publish = false
28	description = "spt-core session-surface mechanism (M3a): the multiplatform PTY layer every hosted agent session runs inside. ConPTY (Windows) / forkpty (Unix) behind one SessionSurface trait, byte-stream output with bounded backpressure, and the live activity buffer (PTY digest) parser primitive. Internal broker machinery (NOT public SDK, R-ARCH-2) — the M3b daemon hosts many of these."
29
30	[dependencies]
31	spt-proto = { path = "../spt-proto" }
32	spt-store = { path = "../spt-store" }
33	spt-msg = { path = "../spt-msg" }
34	portable-pty = "0.8"
35	regex = "1"
36	# The digest data types (Digest/Turn/DigestEntry/ToolUse) cross the broker↔brain
37	# IPC + the `spt digest` control channel (M3b B8), so they are serde-serializable.
38	serde = { version = "1", features = ["derive"] }
39
40	[dev-dependencies]
41	tempfile = "3"
42	===SPT-TERM SRC===
43	digest.rs
44	lib.rs
45	pty.rs
46	reader.rs
47	stream.rs
48	surface.rs
49

===ROOT CARGO===
# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
[workspace]
resolver = "2"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock"]
===CRATES===
spt
spt-daemon
spt-live
spt-msg
spt-proto
spt-runtime
spt-store
spt-term
===SPT-TERM CARGO===
[package]
name = "spt-term"
version = "0.0.0"
edition = "2021"
publish = false
description = "spt-core session-surface mechanism (M3a): the multiplatform PTY layer every hosted agent session runs inside. ConPTY (Windows) / forkpty (Unix) behind one SessionSurface trait, byte-stream output with bounded backpressure, and the live activity buffer (PTY digest) parser primitive. Internal broker machinery (NOT public SDK, R-ARCH-2) — the M3b daemon hosts many of these."

[dependencies]
spt-proto = { path = "../spt-proto" }
spt-store = { path = "../spt-store" }
spt-msg = { path = "../spt-msg" }
portable-pty = "0.8"
regex = "1"
# The digest data types (Digest/Turn/DigestEntry/ToolUse) cross the broker↔brain
# IPC + the `spt digest` control channel (M3b B8), so they are serde-serializable.
serde = { version = "1", features = ["derive"] }

[dev-dependencies]
tempfile = "3"
===SPT-TERM SRC===
digest.rs
lib.rs
pty.rs
reader.rs
stream.rs
surface.rs


Prompt:
<task-notification>
<task-id>bs1xjrp90</task-id>
<tool-use-id>toolu_013YqhCtzqy9oSxE7HChh7wm</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\bs1xjrp90.output</output-file>
<status>completed</status>
<summary>Background command "Inspect workspace + spt-term scaffold" completed (exit code 0)</summary>
</task-notification>


Workspace pattern clear. Checking traceable-reqs registry for M4 req IDs (R-NET-*, R-PAIR-*, REQ-INST-*, REQ-UPD-1) before authoring expanded task layer.


grep -n -E 'R-NET|R-PAIR|REQ-INST|REQ-UPD-1|REQ-NET|REQ-PAIR|REQ-SUBNET|REQ-NOTIF|REQ-EP-4|R-ARCH' traceable-reqs.toml 2>/dev/null | head -60

Find M4 requirement IDs in registry


Command running in background with ID: brr4kizn2. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\brr4kizn2.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>brr4kizn2</task-id>
<tool-use-id>toolu_01JuPfgUHxjLC6Ay8emF6efu</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\brr4kizn2.output</output-file>
<status>completed</status>
<summary>Background command "Find M4 requirement IDs in registry" completed (exit code 0)</summary>
</task-notification>


1	152:id = "REQ-EP-4"
2	162:id = "REQ-INST-1"
3	166:id = "REQ-INST-2"
4	170:id = "REQ-INST-3"
5	174:id = "REQ-INST-4"
6	178:id = "REQ-INST-5"
7	182:id = "REQ-INST-6"
8	186:id = "REQ-INST-7"
9	190:id = "REQ-INST-8"
10	195:id = "REQ-INST-9"
11	199:id = "REQ-INST-10"
12	203:id = "REQ-INST-11"
13	207:id = "REQ-INST-12"
14	211:id = "REQ-INST-13"
15	215:id = "REQ-INST-14"
16	219:id = "REQ-INST-15"
17	252:required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
18	254:id = "REQ-NET-1"
19	258:id = "REQ-NET-2"
20	262:id = "REQ-NET-3"
21	266:id = "REQ-PAIR-1"
22	270:id = "REQ-PAIR-2"
23	274:id = "REQ-PAIR-3"
24	278:id = "REQ-PAIR-4"
25	283:id = "REQ-PAIR-5"
26	287:id = "REQ-PAIR-6"
27	291:id = "REQ-PAIR-7"
28	301:id = "REQ-NOTIF-1"
29	305:id = "REQ-NOTIF-2"
30	311:id = "REQ-UPD-1"
31	356:id = "REQ-INSTALL-1"
32	360:id = "REQ-INSTALL-2"
33	364:id = "REQ-INSTALL-3"
34	368:id = "REQ-INSTALL-4"
35

152:id = "REQ-EP-4"
162:id = "REQ-INST-1"
166:id = "REQ-INST-2"
170:id = "REQ-INST-3"
174:id = "REQ-INST-4"
178:id = "REQ-INST-5"
182:id = "REQ-INST-6"
186:id = "REQ-INST-7"
190:id = "REQ-INST-8"
195:id = "REQ-INST-9"
199:id = "REQ-INST-10"
203:id = "REQ-INST-11"
207:id = "REQ-INST-12"
211:id = "REQ-INST-13"
215:id = "REQ-INST-14"
219:id = "REQ-INST-15"
252:required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
254:id = "REQ-NET-1"
258:id = "REQ-NET-2"
262:id = "REQ-NET-3"
266:id = "REQ-PAIR-1"
270:id = "REQ-PAIR-2"
274:id = "REQ-PAIR-3"
278:id = "REQ-PAIR-4"
283:id = "REQ-PAIR-5"
287:id = "REQ-PAIR-6"
291:id = "REQ-PAIR-7"
301:id = "REQ-NOTIF-1"
305:id = "REQ-NOTIF-2"
311:id = "REQ-UPD-1"
356:id = "REQ-INSTALL-1"
360:id = "REQ-INSTALL-2"
364:id = "REQ-INSTALL-3"
368:id = "REQ-INSTALL-4"


Prompt:
<task-notification>
<task-id>brr4kizn2</task-id>
<tool-use-id>toolu_01JuPfgUHxjLC6Ay8emF6efu</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\brr4kizn2.output</output-file>
<status>completed</status>
<summary>Background command "Find M4 requirement IDs in registry" completed (exit code 0)</summary>
</task-notification>


Reading the full M4 requirement blocks (INST/NET/PAIR/NOTIF/UPD/EP-4) to author tasks against real ids + current stages.


148	id = "REQ-EP-3"
149	title = "Messaging payloads carry typed operation commands + file blobs"
150	required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
151	[[requirements]]
152	id = "REQ-EP-4"
153	title = "PresenceChannel broker endpoint (seam day-one)"
154	required_stages = []
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 = []
159
160	# ───────────────────────────── Instances ────────────────────────────────────
161	[[requirements]]
162	id = "REQ-INST-1"
163	title = "endpoint ID vs instance split (adapter-agnostic ID)"
164	required_stages = []
165	[[requirements]]
166	id = "REQ-INST-2"
167	title = "Per-node files, synced Psyche mind"
168	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 = []
177	[[requirements]]
178	id = "REQ-INST-5"
179	title = "Two-tier context sync (live to all, project to same-project)"
180	required_stages = []
181	[[requirements]]
182	id = "REQ-INST-6"
183	title = "Deferred messages not delivered to dormant/suspended instances"
184	required_stages = []
185	[[requirements]]
186	id = "REQ-INST-7"
187	title = "Subnet registry + bare-id resolution policy"
188	required_stages = []
189	[[requirements]]
190	id = "REQ-INST-8"
191	title = "Remote-control mode distinct from local operation"
192	required_stages = []
193	# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
194	[[requirements]]
195	id = "REQ-INST-9"
196	title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
197	required_stages = []
198	[[requirements]]
199	id = "REQ-INST-10"
200	title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
201	required_stages = []
202	[[requirements]]
203	id = "REQ-INST-11"
204	title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
205	required_stages = []
206	[[requirements]]
207	id = "REQ-INST-12"
208	title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
209	required_stages = []
210	[[requirements]]
211	id = "REQ-INST-13"
212	title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
213	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 = []
222	[[requirements]]
223	id = "REQ-REACH-1"
224	title = "Off-node remote-drive detection + file transfer"
225	required_stages = []
226	[[requirements]]
227	id = "REQ-REACH-2"
228	title = "Remote command execution (deferred, consent-gated)"
229	required_stages = []
230
231	# ───────────────────────────── Local messaging (M1) ─────────────────────────
232	# No PRD R-* covers local message delivery (foundational/implicit). Registered
233	# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
234	# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
235	[[requirements]]
236	id = "REQ-MSG-1"
237	title = "Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)"
238	required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
239	[[requirements]]
240	id = "REQ-MSG-2"
241	title = "spt binary CLI surface: send/ring/ready/poll/list/stop/whoami, stable arg shapes + exit codes"
242	required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired
243	[[requirements]]
244	id = "REQ-MSG-3"
245	title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
246	required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E
247
248	# ───────────────────────────── Networking / pairing ─────────────────────────
249	[[requirements]]
250	id = "REQ-NODE-IDENTITY"
251	title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
252	required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
253	[[requirements]]
254	id = "REQ-NET-1"
255	title = "WAN messaging first-class, behind default-on net feature flag"
256	required_stages = []
257	[[requirements]]
258	id = "REQ-NET-2"
259	title = "n0 relay default + self-host knob + plain-language disclosure"
260	required_stages = []
261	[[requirements]]
262	id = "REQ-NET-3"
263	title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
264	required_stages = []
265	[[requirements]]
266	id = "REQ-PAIR-1"
267	title = "TOTP-seeded SPAKE2 pairing"
268	required_stages = []
269	[[requirements]]
270	id = "REQ-PAIR-2"
271	title = "Local trust store with TOFU + warn-on-change"
272	required_stages = []
273	[[requirements]]
274	id = "REQ-PAIR-3"
275	title = "Fetch current pairing code from any paired node"
276	required_stages = []
277	[[requirements]]
278	id = "REQ-PAIR-4"
279	title = "Subnet naming on first pairing"
280	required_stages = []
281	# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
282	[[requirements]]
283	id = "REQ-PAIR-5"
284	title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
285	required_stages = []
286	[[requirements]]
287	id = "REQ-PAIR-6"
288	title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
289	required_stages = []
290	[[requirements]]
291	id = "REQ-PAIR-7"
292	title = "Subnet icon (inline image metadata, GUI-only consumer)"
293	required_stages = []
294	# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
295	[[requirements]]
296	id = "REQ-SEC-1"
297	title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
298	required_stages = []
299	# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
300	[[requirements]]
301	id = "REQ-NOTIF-1"
302	title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
303	required_stages = []
304	[[requirements]]
305	id = "REQ-NOTIF-2"
306	title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
307	required_stages = []
308
309	# ───────────────────────────── Self-update ──────────────────────────────────
310	[[requirements]]
311	id = "REQ-UPD-1"
312	title = "Peer-propagated update over P2P"
313	required_stages = []
314	[[requirements]]
315	id = "REQ-UPD-2"
316	title = "All binaries signature-verified before handoff"
317	required_stages = ["impl", "unit"]  # activated M3c-C1: release.rs Ed25519 verify-before-handoff (signature + key trust + SHA-256 artifact binding); update::plan_verified is the front door that produces a plan ONLY for a verified release, so an unverified binary never reaches apply_brain_only
318	[[requirements]]
319	id = "REQ-UPD-3"
320	title = "No endpoint process terminates/suspends during self-update"
321	required_stages = ["impl", "unit", "int"]  # activated M3c-C0: the update engine's brain-only apply swaps logic over the B2 handoff substrate; the C0 E2E (tests/update.rs) proves a live PTY child's pid is unchanged + its output stream gapless across the swap (zero endpoint interruption — ADR-0004 §A, absolute for the brain-only class)
322	[[requirements]]
323	id = "REQ-UPD-4"
324	title = "Update gated on user confirmation by default; opt-in full-auto"
325	required_stages = ["impl", "unit"]  # activated M3c-C2: consent::decide is the pure gate (default NeedsConsent, full-auto opt-in via DaemonConfig.full_auto_update); consent::most_recently_active resolves the prompt target over live perches + the new info.json last_active_ms recency stamp (set_last_active, stamped each pulse tick). Interactive prompt UX itself rides the deferred notif/PresenceChannel seam
326	[[requirements]]
327	id = "REQ-UPD-5"
328	title = "spt-core ripple-updates registered adapters"
329	required_stages = ["impl", "unit"]  # activated M3c-C3: adapter_update::conduct_ripple conducts self-then-adapters in registration order; file_pull payloads get adapter content signing (verify_signature against the per-adapter manifest key + verify_artifact digest), delegated requires a self_verifies attest
330
331	# ───────────────────────────── Terminal wrapper / frontend ──────────────────
332	[[requirements]]

id = "REQ-EP-3"
title = "Messaging payloads carry typed operation commands + file blobs"
required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
[[requirements]]
id = "REQ-EP-4"
title = "PresenceChannel broker endpoint (seam day-one)"
required_stages = []
[[requirements]]
id = "REQ-EP-5"
title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
required_stages = []

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = []
[[requirements]]
id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = []
[[requirements]]
id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = []
[[requirements]]
id = "REQ-INST-5"
title = "Two-tier context sync (live to all, project to same-project)"
required_stages = []
[[requirements]]
id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = []
[[requirements]]
id = "REQ-INST-7"
title = "Subnet registry + bare-id resolution policy"
required_stages = []
[[requirements]]
id = "REQ-INST-8"
title = "Remote-control mode distinct from local operation"
required_stages = []
# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-INST-9"
title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
required_stages = []
[[requirements]]
id = "REQ-INST-10"
title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
required_stages = []
[[requirements]]
id = "REQ-INST-11"
title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
required_stages = []
[[requirements]]
id = "REQ-INST-12"
title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
required_stages = []
[[requirements]]
id = "REQ-INST-13"
title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
required_stages = []
[[requirements]]
id = "REQ-INST-14"
title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
required_stages = []
[[requirements]]
id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = []
[[requirements]]
id = "REQ-REACH-1"
title = "Off-node remote-drive detection + file transfer"
required_stages = []
[[requirements]]
id = "REQ-REACH-2"
title = "Remote command execution (deferred, consent-gated)"
required_stages = []

# ───────────────────────────── Local messaging (M1) ─────────────────────────
# No PRD R-* covers local message delivery (foundational/implicit). Registered
# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
[[requirements]]
id = "REQ-MSG-1"
title = "Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)"
required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
[[requirements]]
id = "REQ-MSG-2"
title = "spt binary CLI surface: send/ring/ready/poll/list/stop/whoami, stable arg shapes + exit codes"
required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired
[[requirements]]
id = "REQ-MSG-3"
title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E

# ───────────────────────────── Networking / pairing ─────────────────────────
[[requirements]]
id = "REQ-NODE-IDENTITY"
title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
[[requirements]]
id = "REQ-NET-1"
title = "WAN messaging first-class, behind default-on net feature flag"
required_stages = []
[[requirements]]
id = "REQ-NET-2"
title = "n0 relay default + self-host knob + plain-language disclosure"
required_stages = []
[[requirements]]
id = "REQ-NET-3"
title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
required_stages = []
[[requirements]]
id = "REQ-PAIR-1"
title = "TOTP-seeded SPAKE2 pairing"
required_stages = []
[[requirements]]
id = "REQ-PAIR-2"
title = "Local trust store with TOFU + warn-on-change"
required_stages = []
[[requirements]]
id = "REQ-PAIR-3"
title = "Fetch current pairing code from any paired node"
required_stages = []
[[requirements]]
id = "REQ-PAIR-4"
title = "Subnet naming on first pairing"
required_stages = []
# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-PAIR-5"
title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
required_stages = []
[[requirements]]
id = "REQ-PAIR-6"
title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
required_stages = []
[[requirements]]
id = "REQ-PAIR-7"
title = "Subnet icon (inline image metadata, GUI-only consumer)"
required_stages = []
# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
[[requirements]]
id = "REQ-SEC-1"
title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
required_stages = []
# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-NOTIF-1"
title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
required_stages = []
[[requirements]]
id = "REQ-NOTIF-2"
title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
required_stages = []

# ───────────────────────────── Self-update ──────────────────────────────────
[[requirements]]
id = "REQ-UPD-1"
title = "Peer-propagated update over P2P"
required_stages = []
[[requirements]]
id = "REQ-UPD-2"
title = "All binaries signature-verified before handoff"
required_stages = ["impl", "unit"]  # activated M3c-C1: release.rs Ed25519 verify-before-handoff (signature + key trust + SHA-256 artifact binding); update::plan_verified is the front door that produces a plan ONLY for a verified release, so an unverified binary never reaches apply_brain_only
[[requirements]]
id = "REQ-UPD-3"
title = "No endpoint process terminates/suspends during self-update"
required_stages = ["impl", "unit", "int"]  # activated M3c-C0: the update engine's brain-only apply swaps logic over the B2 handoff substrate; the C0 E2E (tests/update.rs) proves a live PTY child's pid is unchanged + its output stream gapless across the swap (zero endpoint interruption — ADR-0004 §A, absolute for the brain-only class)
[[requirements]]
id = "REQ-UPD-4"
title = "Update gated on user confirmation by default; opt-in full-auto"
required_stages = ["impl", "unit"]  # activated M3c-C2: consent::decide is the pure gate (default NeedsConsent, full-auto opt-in via DaemonConfig.full_auto_update); consent::most_recently_active resolves the prompt target over live perches + the new info.json last_active_ms recency stamp (set_last_active, stamped each pulse tick). Interactive prompt UX itself rides the deferred notif/PresenceChannel seam
[[requirements]]
id = "REQ-UPD-5"
title = "spt-core ripple-updates registered adapters"
required_stages = ["impl", "unit"]  # activated M3c-C3: adapter_update::conduct_ripple conducts self-then-adapters in registration order; file_pull payloads get adapter content signing (verify_signature against the per-adapter manifest key + verify_artifact digest), delegated requires a self_verifies attest

# ───────────────────────────── Terminal wrapper / frontend ──────────────────
[[requirements]]


<task-notification>
<task-id>bvb7i8r4p</task-id>
<tool-use-id>toolu_01XWhaJceaKAznZPhxk1vweV</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\bvb7i8r4p.output</output-file>
<status>completed</status>
<summary>Background command "Read hazard 6.5 + list REQ-HAZARD ids" completed (exit code 0)</summary>
</task-notification>


Full M4 req landscape captured. Scope bigger than bare D0-D8 — multi-subnet (INST-9..15), pairing amendments (PAIR-5/6/7), notif (NOTIF-1/2), endpoint ACL (SEC-1) all activate at M4 per ADRs. Red-team items may need REQ-HAZARD ids. Checking KNOWN-HAZARDS 6.5 + existing hazard reqs before authoring the expanded plan.


echo "===HAZARD 6.5==="; grep -n -A8 '6.5' docs/KNOWN-HAZARDS.md 2>/dev/null | head -30; echo "===REQ-HAZARD ids in toml==="; grep -n 'REQ-HAZARD' traceable-reqs.toml | head -40

Read hazard 6.5 + list REQ-HAZARD ids


Command running in background with ID: bvb7i8r4p. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\bvb7i8r4p.output. You will be notified when it completes. To check interim output, use Read on that file path.


Prompt:
<task-notification>
<task-id>bvb7i8r4p</task-id>
<tool-use-id>toolu_01XWhaJceaKAznZPhxk1vweV</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\bvb7i8r4p.output</output-file>
<status>completed</status>
<summary>Background command "Read hazard 6.5 + list REQ-HAZARD ids" completed (exit code 0)</summary>
</task-notification>


1	===HAZARD 6.5===
2	202:- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).
3	203-
4	204:### 6.5 Direct-write precedence guard against stale LLM overwrites
5	205-- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
6	206-- **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.
7	207-- **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.
8	208-- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.
9	209-
10	210----
11	211-
12	212-## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
13	--
14	251:| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
15	252-| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
16	253-| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
17	===REQ-HAZARD ids in toml===
18	408:id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
19	412:id = "REQ-HAZARD-INFO-JSON-TORN-READ"
20	416:id = "REQ-HAZARD-STALE-INDEX-LOCK"
21	420:id = "REQ-HAZARD-DEFERRED-DRAIN"
22	424:id = "REQ-HAZARD-WORKER-PATH"
23	428:id = "REQ-HAZARD-PARENT-PID-PREFER"
24	432:id = "REQ-HAZARD-STDIN-SESSION-ID"
25	436:id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
26	440:id = "REQ-HAZARD-GEN-START-NOW"
27	444:id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
28	448:id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
29	452:id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
30	456:id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
31	460:id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
32	464:id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
33	468:id = "REQ-HAZARD-ID-CHARSET"
34	472:id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
35	476:id = "REQ-HAZARD-REGISTRY-CONCURRENT"
36	480:id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
37	484:id = "REQ-HAZARD-INBOX-NO-DOUBLE"
38	488:id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
39	492:id = "REQ-HAZARD-EBUSY-RENAME"
40	496:id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
41	500:id = "REQ-HAZARD-UNC-PATH-STRIP"
42	504:id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
43	508:id = "REQ-HAZARD-SOFT-CLEANUP"
44	512:id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
45	516:id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
46	520:id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
47	525:id = "REQ-HAZARD-CONPTY-DSR"
48	529:id = "REQ-HAZARD-LOCAL-API-AUTH"
49	533:id = "REQ-HAZARD-RESTART-IDEMPOTENT"
50	537:id = "REQ-HAZARD-UPDATE-ROLLBACK"
51	541:id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
52

===HAZARD 6.5===
202:- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).
203-
204:### 6.5 Direct-write precedence guard against stale LLM overwrites
205-- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
206-- **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.
207-- **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.
208-- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.
209-
210----
211-
212-## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
--
251:| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
252-| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
253-| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
===REQ-HAZARD ids in toml===
408:id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
412:id = "REQ-HAZARD-INFO-JSON-TORN-READ"
416:id = "REQ-HAZARD-STALE-INDEX-LOCK"
420:id = "REQ-HAZARD-DEFERRED-DRAIN"
424:id = "REQ-HAZARD-WORKER-PATH"
428:id = "REQ-HAZARD-PARENT-PID-PREFER"
432:id = "REQ-HAZARD-STDIN-SESSION-ID"
436:id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
440:id = "REQ-HAZARD-GEN-START-NOW"
444:id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
448:id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
452:id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
456:id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
460:id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
464:id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
468:id = "REQ-HAZARD-ID-CHARSET"
472:id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
476:id = "REQ-HAZARD-REGISTRY-CONCURRENT"
480:id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
484:id = "REQ-HAZARD-INBOX-NO-DOUBLE"
488:id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
492:id = "REQ-HAZARD-EBUSY-RENAME"
496:id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
500:id = "REQ-HAZARD-UNC-PATH-STRIP"
504:id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
508:id = "REQ-HAZARD-SOFT-CLEANUP"
512:id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
516:id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
520:id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
525:id = "REQ-HAZARD-CONPTY-DSR"
529:id = "REQ-HAZARD-LOCAL-API-AUTH"
533:id = "REQ-HAZARD-RESTART-IDEMPOTENT"
537:id = "REQ-HAZARD-UPDATE-ROLLBACK"
541:id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"


`REQ-HAZARD-DIRECT-WRITE-PRECEDENCE` exists (=6.5). Pairing red-team #10/#11/#12 have no hazard ids yet — register at D2 authoring per stub note + TRACEABILITY rule 3. Enough context. Authoring the expanded M4-PLAN.md task layer, then executing D0 scaffold.

Let me create task tracking first.


ToolSearch

Question: select:TaskCreate,TaskUpdate


Question: select:TaskCreate,TaskUpdate


TaskCreate

Expand M4-PLAN.md D-task layer

Replace the stub Tasks table with a full layered D0-D8 breakdown: sub-tasks, requirement mapping (NET/PAIR/INST/NOTIF/SEC/UPD), red-team obligations (pairing #10/#11/#12, instances #7/#8/#9), acceptance gates, sequencing.


Task #1 created successfully: Expand M4-PLAN.md D-task layer

Expand M4-PLAN.md D-task layer


D0: scaffold crates/spt-net

New crate sibling of spt-term/spt-runtime, above spt-msg. Wire into workspace members, net feature flag, acyclic layering, compiles, traceable-reqs check green. Activate REQ-NET-1 doc stage as scaffold lands.


Task #2 created successfully: D0: scaffold crates/spt-net

D0: scaffold crates/spt-net


Updated task #1 status


Now authoring the expanded task layer. Replacing stub Tasks section with full breakdown + req map + red-team obligations.


Edit

## Tasks (to be expanded at M4 start)

| # | Task | Reqs / hazards | Acceptance |
|---|------|----------------|------------|
| D0 | Scaffold `crates/spt-net` (sibling of spt-term/spt-runtime, above spt-msg) | — | compiles; layering acyclic; `check` green |
| D1 | Iroh endpoint + mDNS discovery (one node advertises, another finds it) | R-NET-* | two local nodes discover + connect |
| D2 | TOTP-SPAKE2 pairing | R-PAIR-* | two machines pair via one TOTP code; trust material stored |
| D3 | Subnet registry + resolution policy (local → most-recently-active → `id@node`) | REQ-INST-7 | an `endpoint_id` resolves to a live instance across nodes |
| D4 | QUIC-stream broker ownership (impl Spike #3's shape into the broker) | ADR-0004 §B | a live cross-node stream survives a brain restart gapless |
| D5 | Cross-node message + remote-drive of a running instance | R-NET-*, REQ-INST-* | drive an agent on machine A from machine B |
| D6 | Cross-node Psyche sync (P2P), retire gh-repo interim | 6.5 (node id) | the mind follows the user across machines (two-tier) |
| D7 | Update peer-propagation transport into the M3c engine seam | REQ-UPD-1 | a signed update self-heals across the subnet; the gate still runs per node |
| D8 | Activation sweep + cross-node E2E; amend ROADMAP (M4 delivered) + CONTEXT | — | the V1-mid scope line met; `check` green; CI matrix green |

## 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 decision pending** (monotonic
  per-node epochs / vector clocks / CRDT-ish / explicit conflict review) — decide at D6
  authoring with the user; two concurrent active writers = conflict, not stale-write.
- **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.

### 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 |
| D8 | REQ-NOTIF-1, REQ-NOTIF-2 |
| 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 |