173:[Omitted long matching line]
322:[Omitted long matching line]
445:- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
549:[Omitted long matching line]
551:[Omitted long matching line]


Bash

Read interim/hub mentions


The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **Liveness is daemon-held, not PID-based.** Unlike the sister's separate wrapper *process*, the Psyche (under ADR-0004) is a **loop inside the daemon** — it has no dedicated liveness pid, and its summarizer subprocess is ephemeral. Its perch is alive iff the daemon's endpoint table says so, surfaced via a daemon-managed `status` field on `info.json` (the same pattern Shells use), **never** an `is_process_alive(info.pid)` check. The M1/M2a interim (no daemon) keeps it a real process, so per-pid liveness holds *interim*; M3 replaces it with daemon-authoritative liveness (KNOWN-HAZARDS 2.5).
- **Hub (opt-in, `spt context-github-setup`):** every node pull→merge→pushes against a private GitHub remote — an always-online sync hub *and* a convenient GUI view. Uses a **shared deploy key** distributed as subnet secret material, so only the setup node needs a GitHub account; other nodes use the key over SSH, with **P2P as backstop**. Hub mode is the **context-repo sync transport only** — messaging, registry, remote-drive, pairing, and presence always ride P2P; gh-on never removes the P2P requirement (ADR-0002, amended).

**adapter registration (`spt adapter add`)**:
How a node comes to *know* an adapter — harness or shell. An explicit **`spt adapter add <path>`** (or **`--github <user/repo>`**) validates the manifest against the published JSON Schema and writes a registration record under `{SPT_HOME}/…/adapters/` — a **copy** of the files for `file_pull`-update adapters (spt-core owns what it later swaps) or a **pointer** for `delegated`-update adapters (the plugin owns + updates its own files). One command + one dir for both `kind="harness"` and `kind="shell"`; the `kind` field differentiates. The `--github` form **fetches the manifest first** (readable-before-install — same rule as the `min_spt_core_version` readable-before-update gate), checks compatibility, then completes the install via the manifest's own `[update]` avenue: **install is the first update** (one fetch/swap mechanism, not two). Harness-bootstrapped install (path a) calls it from the plugin's bootstrap; standalone install (path b) calls it for shell-only / Pi nodes. Registration is **node-local** — it means *"this node can drive/launch this adapter,"* distinct from advertising an endpoint into a subnet. The **registered-adapter set** the self-updater ripple-updates (see Self-update) is exactly this record set.

**Removal (`spt adapter remove`) is soft-deregister:** the adapter is **hidden from new-creation / picker lists immediately**, but existing and live instances **keep running** under it (lazy — nothing is force-torn-down). An optional manifest **`uninstall` command template** (the inverse of the install/`[update]` avenue — e.g. `claude plugin uninstall spt`) cleans the adapter's own artifacts; it runs **once no instance is live** under the adapter (or immediately with `--force`, which cascades teardown). Once actually uninstalled, an endpoint whose synced **adapter history** still references it renders as **"needs install"** in the resume picker and offers `adapter add` (the `--github` path closes the loop).

## Project infrastructure


Read hub-mode section context


### Cross-node Psyche sync

**Sync scope is subnet-exclusive by default.** An endpoint's mind replicates only within **one subnet** unless told otherwise. Endpoint config carries a **subnet-membership list** for sync: the context syncs across the nodes of *every* subnet in that list, defaulting to just the endpoint's home subnet. This is distinct from registry *visibility* (*endpoint visibility*) — being addressable in a subnet does not imply replicating your mind there; mind-replication into a subnet requires that subnet to be in the sync list. The default keeps a `home`-fleet agent's mind off the `work` fleet even when a node bridges both. (The subnet scope composes with the live/project tier split below: a tier syncs to the eligible instances *that are also in an in-scope subnet*.)

The context is a local git repo with `a-<endpoint_id>` (per-agent) and `p-<project_id>` (per-project) branch views. Sync has **two transport modes** (full mechanics: `docs/STORAGE.md`):
- **P2P (default):** the data syncs between a user's nodes over the built-in P2P networking — **no `gh`/account/setup**, the no-central-operator promise. Replaces modern SPT's required GitHub-remote sync.
- **Hub (opt-in, `spt context-github-setup`):** every node pull→merge→pushes against a private GitHub remote — an always-online sync hub *and* a convenient GUI view. Uses a **shared deploy key** distributed as subnet secret material, so only the setup node needs a GitHub account; other nodes use the key over SSH, with **P2P as backstop**. Hub mode is the **context-repo sync transport only** — messaging, registry, remote-drive, pairing, and presence always ride P2P; gh-on never removes the P2P requirement (ADR-0002, amended).

Merge never line-merges a mind file: a custom merge driver resolves context files **per file by the precedence marker**, which carries `source`, **`node`**, and a per-node **version vector** (entries from each node's monotonic epoch source — wall-clock is never the ordering authority, at most a human tiebreaker hint). A dominating write supersedes; a dominated write drops; **concurrent writes (neither dominates) surface as an explicit conflict — never silent newest-wins**: both versions persist as replicated conflict artifacts and the endpoint's **own Psyche reconciles its own mind** in one bounded turn, run by the **active instance's node only** (single reconciler; the merged write dominates both parents and clears the conflict subnet-wide; reconcile unavailable ⟹ the artifacts simply persist, nothing is lost). KNOWN-HAZARDS 6.5 generalized; mechanics: ADR-0013 + `docs/STORAGE.md`. The single-writer invariant (active-instance-authoritative) makes true conflicts rare. The per-project branch view lets any agent synthesize broader context from *other* agents' contexts in the same project and supports query-routing ("which endpoint owns this task"). The two-tier sync maps onto the views: live context (`a-`) → all instances; project context (`p-`) → same-project instances.

### Subnet notifications

**notification (notif)**:
A first-class kind **distinct from an inter-agent message**: a **user-directed, dismissable, resurfacing** subnet event (e.g. *new node paired*, *external-subnet pairing request*, *update available*, *consent needed*). Messages route agent→agent and are consumed once; notifs route **to the user** — via whichever endpoint is active — and **persist until dismissed**. The notif is the general primitive that the previously-ad-hoc "deliver to the user's most-recently-active session" flows are special cases of: **the self-update prompt and the consent escalation are refactored to be notif producers** (one primitive, many producers — kills the duplicated delivery paths; the registry-resolution precursor to PresenceChannel is this primitive's delivery step).


Edit

Merge never line-merges a mind file: a custom merge driver resolves context files **per file by the precedence marker**,

**The gh-repo interim is retired** (M4-D9-6, after the two-host rig proof): during the pre-P2P era the sister project's `gh-repo-sync` skill (and the `spt-agent-storage` private repo wired by `psyche-sync-setup`) was *the* cross-machine context transport. With P2P sync proven end-to-end on real hardware (`docs/TWO-HOST-RUNBOOK.md`), that interim stands down — the sister skill is no longer part of the sync path. `psyche-sync-setup` remains only as the **hub-mode seam** (the opt-in transport above), not the default and never a requirement.

Merge never line-merges a mind file: a custom merge driver resolves context files **per file by the precedence marker**,


The file C:\Users\decid\Documents\projects\spt-core\CONTEXT.md has been updated successfully. (file state is current in your context — no need to Read it back)

**The gh-repo interim is retired** (M4-D9-6, after the two-host rig proof): during the pre-P2P era the sister project's `gh-repo-sync` skill (and the `spt-agent-storage` private repo wired by `psyche-sync-setup`) was *the* cross-machine context transport. With P2P sync proven end-to-end on real hardware (`docs/TWO-HOST-RUNBOOK.md`), that interim stands down — the sister skill is no longer part of the sync path. `psyche-sync-setup` remains only as the **hub-mode seam** (the opt-in transport above), not the default and never a requirement.

Merge never line-merges a mind file: a custom merge driver resolves context files **per file by the precedence marker**,

- **P2P (default):** the data syncs between a user's nodes over the built-in P2P networking — **no `gh`/account/setup**, the no-central-operator promise. Replaces modern SPT's required GitHub-remote sync.

- **Hub (opt-in, `spt context-github-setup`):** every node pull→merge→pushes against a private GitHub remote — an always-online sync hub *and* a convenient GUI view. Uses a **shared deploy key** distributed as subnet secret material, so only the setup node needs a GitHub account; other nodes use the key over SSH, with **P2P as backstop**. Hub mode is the **context-repo sync transport only** — messaging, registry, remote-drive, pairing, and presence always ride P2P; gh-on never removes the P2P requirement (ADR-0002, amended).

+**The gh-repo interim is retired** (M4-D9-6, after the two-host rig proof): during the pre-P2P era the sister project's `gh-repo-sync` skill (and the `spt-agent-storage` private repo wired by `psyche-sync-setup`) was *the* cross-machine context transport. With P2P sync proven end-to-end on real hardware (`docs/TWO-HOST-RUNBOOK.md`), that interim stands down — the sister skill is no longer part of the sync path. `psyche-sync-setup` remains only as the **hub-mode seam** (the opt-in transport above), not the default and never a requirement.

Merge never line-merges a mind file: a custom merge driver resolves context files **per file by the precedence marker**, which carries `source`, **`node`**, and a per-node **version vector** (entries from each node's monotonic epoch source — wall-clock is never the ordering authority, at most a human tiebreaker hint). A dominating write supersedes; a dominated write drops; **concurrent writes (neither dominates) surface as an explicit conflict — never silent newest-wins**: both versions persist as replicated conflict artifacts and the endpoint's **own Psyche reconciles its own mind** in one bounded turn, run by the **active instance's node only** (single reconciler; the merged write dominates both parents and clears the conflict subnet-wide; reconcile unavailable ⟹ the artifacts simply persist, nothing is lost). KNOWN-HAZARDS 6.5 generalized; mechanics: ADR-0013 + `docs/STORAGE.md`. The single-writer invariant (active-instance-authoritative) makes true conflicts rare. The per-project branch view lets any agent synthesize broader context from *other* agents' contexts in the same project and supports query-routing ("which endpoint owns this task"). The two-tier sync maps onto the views: live context (`a-`) → all instances; project context (`p-`) → same-project instances.


Bash

gh run list --commit a86371c9cfd87639c4d5dd5e2c82d464bc028ed0 --json databaseId --jq '.[0].databaseId' | xargs -I{} gh run view {} --json jobs --jq '.jobs[] | "\(.name): \(.status) \(.conclusion)"'

Check rig run job states


test (self-hosted, Windows, hfenduleam): in_progress
traceability: queued
test (self-hosted, Linux, gravity): in_progress


Edits staged locally (toml int activations + CONTEXT retirement note), all gated on rig green. Test legs in progress; twohost jobs release after both. Waiting on watcher.


<task-notification>
<task-id>badwkq1hv</task-id>
<tool-use-id>toolu_01B3UELXJ3gtt3nk7tFHnJVi</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\badwkq1hv.output</output-file>
<status>completed</status>
<summary>Background command "Wait for rig run FINAL on a86371c" completed (exit code 0)</summary>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


tail -1 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\badwkq1hv.output"; gh run list --commit a86371c9cfd87639c4d5dd5e2c82d464bc028ed0 --json databaseId --jq '.[0].databaseId' | xargs -I{} gh run view {} --json jobs --jq '.jobs[] | "\(.name): \(.conclusion)"'

Rig run FINAL + per-job results


FINAL: success run:26958175812
test (self-hosted, Windows, hfenduleam): success
traceability: success
test (self-hosted, Linux, gravity): success
twohost-b: success
twohost-a: success


**RIG PROOF GREEN — first attempt.** Both twohost jobs success. Capturing evidence:


gh run view 26958175812 --log --job $(gh run view 26958175812 --json jobs --jq '.jobs[] | select(.name=="twohost-a") | .databaseId') 2>/dev/null | grep -E "TWOHOST|test result" | sed 's/^twohost-a\t[^\t]*\t//' | head -25

Extract role A evidence lines


gh run view 26958175812 --log --job $(gh run view 26958175812 --json jobs --jq '.jobs[] | select(.name=="twohost-b") | .databaseId') 2>/dev/null | grep -E "TWOHOST|test result" | sed 's/^twohost-b\t[^\t]*\t//' | head -25

Extract role B evidence lines


2026-06-04T14:33:51.3603681Z TWOHOST role A: node bcead52b862344aef51998ca5d8f15dce1e38e6cee3795b219a5849f6ad4b41a
2026-06-04T14:35:18.7502572Z TWOHOST OK: pairing (initiator) — Pinned
2026-06-04T14:35:25.2691944Z TWOHOST OK: register: B's perch row replicated to A
2026-06-04T14:35:25.3043840Z TWOHOST OK: message sent (B asserts the spool)
2026-06-04T14:35:25.3617736Z TWOHOST OK: file fetch (sid 1)
2026-06-04T14:35:25.4115489Z TWOHOST OK: remote-drive (attach echo)
2026-06-04T14:35:25.7063120Z TWOHOST OK: sync: A pulled B's mind
2026-06-04T14:35:25.7063670Z TWOHOST OK: update: B's v6 staged at A through the verify gate
2026-06-04T14:35:25.7064809Z TWOHOST OK: update: consent notif surfaced at A
2026-06-04T14:35:26.2069234Z TWOHOST OK: notif: B's dismissal replicated to A
2026-06-04T14:35:26.2858722Z TWOHOST OK: file push (done barrier)
2026-06-04T14:35:26.2859180Z TWOHOST role A: ladder complete
2026-06-04T14:35:26.3046607Z test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 94.95s


2026-06-04T14:35:16.2841311Z TWOHOST role B: node 9bbcee970607c7454b0baf8b38b032ccdf1a36de7ef04d614158fecd1f08b612
2026-06-04T14:35:16.4299711Z TWOHOST role B: drive session 1 ready
2026-06-04T14:35:18.7506993Z TWOHOST OK: pairing (responder) — Pinned
2026-06-04T14:35:22.4736833Z TWOHOST OK: register: A's perch row replicated to B
2026-06-04T14:35:25.6273642Z TWOHOST OK: message: A's WAN message spooled at B
2026-06-04T14:35:25.6285215Z TWOHOST OK: notif: A's insert fired on B
2026-06-04T14:35:25.6614056Z TWOHOST OK: notif dismissed at B (bcead52b862344aef51998ca5d8f15dce1e38e6cee3795b219a5849f6ad4b41a:1780583722231) — replicating back
2026-06-04T14:35:25.6720997Z TWOHOST OK: sync: B pulled A's mind
2026-06-04T14:35:26.4228358Z TWOHOST OK: done-file pushed by A (ladder complete on A)
2026-06-04T14:35:26.4229959Z TWOHOST role B: ladder complete
2026-06-04T14:35:26.4290720Z test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.15s


Write SPT_TWO_HOST=1 gated int tests (D9b ladder)

tests gated on SPT_TWO_HOST=1 + role/peer-addr env, silent skip otherwise. Ladder: pair → register → cross-node message → remote-drive → Psyche sync → update self-heal + notif cross-node leg (insert A→fires B; dismiss B→replicates A). Int tags [int->REQ-NET-1], [int->REQ-INST-5], [int->REQ-UPD-1], [int->REQ-NOTIF-1] + pairing/registry as touched. Stages NOT activated until rig run green.

Write docs/TWO-HOST-RUNBOOK.md

Exact rig procedure HFENDULEAM ↔ gravity-linux: env setup, run order, what green looks like. Evidence appendix filled after real run. Consider [twohost] commit-message CI gate for gravity leg (no SSH from HFENDULEAM).

Run two-host proof on rig, capture evidence

Execute runbook on HFENDULEAM + gravity (likely via [twohost] CI gate for gravity leg). Capture run output into runbook appendix. Then activate int stages on touched reqs in traceable-reqs.toml. Flake-ledger discipline for any rig flake.

Retire gh-repo interim sync (CONTEXT.md note)

After proof green: note in CONTEXT.md — sister gh-repo-sync skill stands down; psyche-sync-setup remains for hub-mode seam but no longer THE sync path.


Full ladder cross-host in ~8s once both sides up (A retried pairing dial ~85s while gravity built). Evidence → runbook appendix:


Edit

## Evidence appendix

> Filled from the first green rig run; later reruns replace it.

*(pending — no rig run captured yet)*

## Evidence appendix

> Filled from the first green rig run; later reruns replace it.

**Run:** CI run `26958175812` on `a86371c` (2026-06-04, Procedure 1 — `[twohost]`
tagged push; both jobs green on the first attempt, no flakes, no firewall rule
needed — role A's pump traffic kept the reverse UDP 4-tuple warm as predicted).
Role A retried the pairing dial for ~85 s while gravity's job built; once both
sides were up the whole ladder converged in ~8 s over tailscale.

Role A (hfenduleam, node `bcead52b…4b41a`):

```
14:33:51 TWOHOST role A: node bcead52b862344aef51998ca5d8f15dce1e38e6cee3795b219a5849f6ad4b41a
14:35:18 TWOHOST OK: pairing (initiator) — Pinned
14:35:25 TWOHOST OK: register: B's perch row replicated to A
14:35:25 TWOHOST OK: message sent (B asserts the spool)
14:35:25 TWOHOST OK: file fetch (sid 1)
14:35:25 TWOHOST OK: remote-drive (attach echo)
14:35:25 TWOHOST OK: sync: A pulled B's mind
14:35:25 TWOHOST OK: update: B's v6 staged at A through the verify gate
14:35:25 TWOHOST OK: update: consent notif surfaced at A
14:35:26 TWOHOST OK: notif: B's dismissal replicated to A
14:35:26 TWOHOST OK: file push (done barrier)
14:35:26 TWOHOST role A: ladder complete
         test result: ok. 2 passed; 0 failed (94.95s)
```

Role B (gravity-linux, node `9bbcee97…8b612`):

```
14:35:16 TWOHOST role B: node 9bbcee970607c7454b0baf8b38b032ccdf1a36de7ef04d614158fecd1f08b612
14:35:16 TWOHOST role B: drive session 1 ready
14:35:18 TWOHOST OK: pairing (responder) — Pinned
14:35:22 TWOHOST OK: register: A's perch row replicated to B
14:35:25 TWOHOST OK: message: A's WAN message spooled at B
14:35:25 TWOHOST OK: notif: A's insert fired on B
14:35:25 TWOHOST OK: notif dismissed at B (bcead52b…:1780583722231) — replicating back
14:35:25 TWOHOST OK: sync: B pulled A's mind
14:35:26 TWOHOST OK: done-file pushed by A (ladder complete on A)
14:35:26 TWOHOST role B: ladder complete
         test result: ok. 2 passed; 0 failed (10.15s)
```


The file C:\Users\decid\Documents\projects\spt-core\docs\TWO-HOST-RUNBOOK.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Evidence appendix

> Filled from the first green rig run; later reruns replace it.

**Run:** CI run `26958175812` on `a86371c` (2026-06-04, Procedure 1 — `[twohost]`
tagged push; both jobs green on the first attempt, no flakes, no firewall rule
needed — role A's pump traffic kept the reverse UDP 4-tuple warm as predicted).
Role A retried the pairing dial for ~85 s while gravity's job built; once both
sides were up the whole ladder converged in ~8 s over tailscale.

Role A (hfenduleam, node `bcead52b…4b41a`):

```
14:33:51 TWOHOST role A: node bcead52b862344aef51998ca5d8f15dce1e38e6cee3795b219a5849f6ad4b41a
14:35:18 TWOHOST OK: pairing (initiator) — Pinned
14:35:25 TWOHOST OK: register: B's perch row replicated to A
14:35:25 TWOHOST OK: message sent (B asserts the spool)
14:35:25 TWOHOST OK: file fetch (sid 1)
14:35:25 TWOHOST OK: remote-drive (attach echo)
14:35:25 TWOHOST OK: sync: A pulled B's mind
14:35:25 TWOHOST OK: update: B's v6 staged at A through the verify gate
14:35:25 TWOHOST OK: update: consent notif surfaced at A
14:35:26 TWOHOST OK: notif: B's dismissal replicated to A
14:35:26 TWOHOST OK: file push (done barrier)
14:35:26 TWOHOST role A: ladder complete
         test result: ok. 2 passed; 0 failed (94.95s)
```

Role B (gravity-linux, node `9bbcee97…8b612`):

```
14:35:16 TWOHOST role B: node 9bbcee970607c7454b0baf8b38b032ccdf1a36de7ef04d614158fecd1f08b612
14:35:16 TWOHOST role B: drive session 1 ready
14:35:18 TWOHOST OK: pairing (responder) — Pinned
14:35:22 TWOHOST OK: register: A's perch row replicated to B
14:35:25 TWOHOST OK: message: A's WAN message spooled at B
14:35:25 TWOHOST OK: notif: A's insert fired on B
14:35:25 TWOHOST OK: notif dismissed at B (bcead52b…:1780583722231) — replicating back
14:35:25 TWOHOST OK: sync: B pulled A's mind
14:35:26 TWOHOST OK: done-file pushed by A (ladder complete on A)
14:35:26 TWOHOST role B: ladder complete
         test result: ok. 2 passed; 0 failed (10.15s)
```

# Two-host rig runbook — the D9b ladder on real hardware

How to run the `SPT_TWO_HOST` integration ladder (`crates/spt-daemon/tests/twohost.rs`)
across two real machines, and what green looks like. This is the M4 cross-node proof
(M4-PLAN §D9b): **pair → register → cross-node message → remote-drive → Psyche sync →
update self-heal → notif cross-node** — every rung over a real network path between two
separate processes on two separate hosts, no loopback shortcuts.

The test is **env-gated**: without `SPT_TWO_HOST=1` + a role it silent-skips (CI's
normal sweeps never run it). The `int` evidence tags live on the gated tests; the
requirement `int` stages activate only after a real rig run is green (TRACEABILITY.md
rule: tags on real evidence).

## The rig

| host | OS | role | tailscale address |
|------|----|------|-------------------|
| HFENDULEAM | Windows 11 | **a** — joiner / driver | `100.68.35.65` |
| gravity-linux | Ubuntu 22.04 | **b** — seed-holder / server | `100.100.62.43` |

Tailscale addresses are the rig's stable transport (the hosts sit on different
physical subnets and gravity's hostname does not resolve from HFENDULEAM; the
tailnet IPs are static). Any reachable IP pair works — LAN addresses included.

## How the two sides find each other (no runtime exchange)

Everything either side needs is **derived from a shared secret + static env**, so both
legs run non-interactively (the gravity leg is driven by a commit-message-gated CI
step — there is no SSH between the rig hosts):

- node identities ← `sha256(secret:"node-a"/"node-b")`;
- the subnet TOTP seed ← `sha256(secret:"subnet-totp")[..20]` — role A computes the
  6-digit code locally, standing in for the user reading the seed-holder's
  `spt pair show-totp` screen. **The wire ceremony is the real SPAKE2/TOTP
  exchange** (run_initiator/run_responder over the pre-trust ALPN, cross-host);
  only the operator's eyeballs are simulated;
- the release signing key ← `sha256(secret:"release")`;
- dialable addresses ← derived node id + the peer's IP + fixed UDP ports
  (`BindScope::Port`): relays and discovery disabled, one direct path each way.

## Env contract

| var | role | meaning | rig value |
|-----|------|---------|-----------|
| `SPT_TWO_HOST` | both | `1` activates; anything else silent-skips | `1` |
| `SPT_TWO_HOST_ROLE` | both | `a` (joiner/driver) / `b` (seed-holder/server) | per host |
| `SPT_TWO_HOST_SECRET` | both | shared secret, identical both sides | any string |
| `SPT_TWO_HOST_PEER_IP` | both | the OTHER host's IP | see table above |
| `SPT_TWO_HOST_SUBNET` | both | subnet name | default `twohost` |
| `SPT_TWO_HOST_PORT_A` | both | A's broker UDP port | default `7460` |
| `SPT_TWO_HOST_PORT_B` | both | B's broker UDP port | default `7461` |
| `SPT_TWO_HOST_PORT_PAIR` | both | B's pre-trust pairing UDP port | default `7462` |
| `SPT_TWO_HOST_WAIT_SECS` | both | per-rung convergence deadline | `900` on the rig (absorbs runner start skew); default `300` |

## Prerequisites

- Clocks NTP-synced (the pairing TOTP window is ±30 s).
- UDP reachable both directions on the three ports. The Windows side may need a
  one-time inbound allow (run elevated):
  `netsh advfirewall firewall add rule name="spt twohost rig" dir=in action=allow protocol=UDP localport=7460-7462`
  In practice A's pump dials B first and B's replies ride the same UDP 4-tuple, so
  a stateful default usually passes — add the rule if role B logs
  `PUMP_PEER_FAIL`/dial timeouts against A.
- Both sides on the same commit (the wire formats must match).

## Procedure 1 — CI-driven rig run (the standard path)

Both rig hosts are the repo's self-hosted runners, so one tagged push runs both legs:

1. Land the change you want proven; wait for normal CI green.
2. Push a commit whose message contains **`[twohost]`** (an empty commit is fine):
   `git commit --allow-empty -m "test(rig): two-host ladder run [twohost]"`
3. The `twohost-a` (hfenduleam) and `twohost-b` (gravity) jobs gate on the tag and
   on `needs: test` — they release together after both test legs finish, which keeps
   the two roles inside each other's `WAIT` window.
4. Verify FINAL conclusions sha-pinned (gotchas #7/#13):
   `gh run list --commit <sha> --json name,conclusion`
5. Capture both job logs' `TWOHOST OK:` lines into the evidence appendix below.

## Procedure 2 — manual two-terminal run

On gravity (start first):

```sh
SPT_TWO_HOST=1 SPT_TWO_HOST_ROLE=b \
SPT_TWO_HOST_SECRET=rig-<date> SPT_TWO_HOST_PEER_IP=100.68.35.65 \
SPT_TWO_HOST_WAIT_SECS=900 \
cargo test -p spt-daemon --test twohost -- --nocapture
```

On HFENDULEAM (PowerShell):

```powershell
$env:SPT_TWO_HOST="1"; $env:SPT_TWO_HOST_ROLE="a"
$env:SPT_TWO_HOST_SECRET="rig-<date>"; $env:SPT_TWO_HOST_PEER_IP="100.100.62.43"
$env:SPT_TWO_HOST_WAIT_SECS="900"
cargo test -p spt-daemon --test twohost -- --nocapture
```

Role A retries the pairing dial until B's ceremony endpoint answers, so exact start
order only matters within the `WAIT` window.

## What green looks like

Role A prints, in order:

```
TWOHOST OK: pairing (initiator) — Pinned
TWOHOST OK: register: B's perch row replicated to A
TWOHOST OK: message sent (B asserts the spool)
TWOHOST OK: file fetch (sid <n>)
TWOHOST OK: remote-drive (attach echo)
TWOHOST OK: sync: A pulled B's mind
TWOHOST OK: update: B's v6 staged at A through the verify gate
TWOHOST OK: update: consent notif surfaced at A
TWOHOST OK: notif: B's dismissal replicated to A
TWOHOST OK: file push (done barrier)
TWOHOST role A: ladder complete
```

Role B prints, in order:

```
TWOHOST OK: pairing (responder) — Pinned
TWOHOST OK: register: A's perch row replicated to B
TWOHOST OK: message: A's WAN message spooled at B
TWOHOST OK: notif: A's insert fired on B
TWOHOST OK: notif dismissed at B (<id>) — replicating back
TWOHOST OK: sync: B pulled A's mind
TWOHOST OK: done-file pushed by A (ladder complete on A)
TWOHOST role B: ladder complete
```

Plus `test result: ok` on both sides. Any rung that never converges panics with
`never converged on the rig: <rung>` — file a flake-ledger entry, don't shrug.

## What each rung proves

| rung | evidence | requirement |
|------|----------|-------------|
| pairing ceremony, cross-host SPAKE2 + seed transfer | trust pinned both sides; joiner holds the subnet seed | REQ-PAIR-1, REQ-PAIR-5 |
| registry replication, both directions | each side's perch row in the other's gated registry | REQ-INST-7 |
| WAN message | A's send lands in B's perch spool through the dispatcher funnel | REQ-NET-1 |
| remote-drive | A fetches a file off B, types into B's live PTY, reads the echo, pushes a file back | REQ-INST-8, REQ-REACH-1 |
| Psyche sync, both directions | each side bootstrap-pulls a mind it never held (registry-derived want-ref, gate served remotely) | REQ-INST-5 |
| update self-heal | B's staged v6 verified + staged at A under the trusted key; consent notif surfaced (gated default) | REQ-UPD-1, REQ-UPD-4 posture |
| notif cross-node | insert on A fires on B; B's dismissal replicates back to A | REQ-NOTIF-1 |

## Evidence appendix

> Filled from the first green rig run; later reruns replace it.

*(pending — no rig run captured yet)*

+tagged push; both jobs green on the first attempt, no flakes, no firewall rule

+needed — role A's pump traffic kept the reverse UDP 4-tuple warm as predicted).

+Role A retried the pairing dial for ~85 s while gravity's job built; once both

+sides were up the whole ladder converged in ~8 s over tailscale.

+Role A (hfenduleam, node `bcead52b…4b41a`):

+14:35:18 TWOHOST OK: pairing (initiator) — Pinned

+14:35:25 TWOHOST OK: register: B's perch row replicated to A

+14:35:25 TWOHOST OK: message sent (B asserts the spool)

+14:35:25 TWOHOST OK: file fetch (sid 1)

+14:35:25 TWOHOST OK: remote-drive (attach echo)

+14:35:25 TWOHOST OK: sync: A pulled B's mind

+14:35:25 TWOHOST OK: update: B's v6 staged at A through the verify gate

+14:35:25 TWOHOST OK: update: consent notif surfaced at A

+14:35:26 TWOHOST OK: notif: B's dismissal replicated to A

+14:35:26 TWOHOST OK: file push (done barrier)

+14:35:26 TWOHOST role A: ladder complete

+         test result: ok. 2 passed; 0 failed (94.95s)

+Role B (gravity-linux, node `9bbcee97…8b612`):

+14:35:16 TWOHOST role B: drive session 1 ready

+14:35:18 TWOHOST OK: pairing (responder) — Pinned

+14:35:22 TWOHOST OK: register: A's perch row replicated to B

+14:35:25 TWOHOST OK: message: A's WAN message spooled at B

+14:35:25 TWOHOST OK: notif: A's insert fired on B

+14:35:25 TWOHOST OK: notif dismissed at B (bcead52b…:1780583722231) — replicating back

+14:35:25 TWOHOST OK: sync: B pulled A's mind

+14:35:26 TWOHOST OK: done-file pushed by A (ladder complete on A)

+14:35:26 TWOHOST role B: ladder complete

+         test result: ok. 2 passed; 0 failed (10.15s)


Plan status + final slice:


# M4-D9 — activation sweep + cross-node E2E + M4 closeout (JIT plan)

**Status:** D9-5 COMPLETE 2026-06-04

# M4-D9 — activation sweep + cross-node E2E + M4 closeout (JIT plan)

**Status:** D9-6 COMPLETE 2026-06-04 (the two-host rig proof is REAL — CI run
`26958175812`, both roles green first attempt):
- D9-6-1 `107240e` — `tests/twohost.rs`: the §D9b ladder as `SPT_TWO_HOST=1`-gated
  tests (silent skip otherwise; identities/addresses derived from a shared secret +
  static env — no runtime exchange, both legs non-interactive); `BindScope::Port`
  (fixed dialable UDP port); `docs/TWO-HOST-RUNBOOK.md`; the `[twohost]`
  commit-message CI gate (both rig hosts are the runners; `needs: test` releases
  the roles together). Ladder proven green locally first (two processes over
  127.0.0.1; the smoke caught a real test-op/pump-op journal collision).
- D9-6-2 `a86371c` — the rig run: pair (real cross-host SPAKE2/TOTP, trust pinned
  both sides + seed transferred) → register (rows replicated both directions) →
  WAN message (spooled at B) → remote-drive (file fetch + attach echo + file push)
  → Psyche sync (both directions bootstrap-pull) → update self-heal (v6 verified +
  staged + consent-notified at A) → notif (insert on A fired on B; dismiss on B
  replicated to A). HFENDULEAM ↔ gravity-linux over tailscale; whole ladder ~8 s
  once both sides up; no firewall rule needed (the pump keeps the reverse UDP
  4-tuple warm). Evidence in the runbook appendix; `docs/FLAKE-LEDGER.md` made
  durable en route (digest flake second occurrence, rerun passed).
- int stages activated post-proof: REQ-NET-1, REQ-INST-5, REQ-INST-7, REQ-INST-8,
  REQ-REACH-1, REQ-PAIR-1, REQ-PAIR-5, REQ-NOTIF-1, REQ-UPD-1 → `[impl, unit, int]`.
- gh-repo interim RETIRED (CONTEXT.md note): the sister gh-repo-sync skill stands
  down; psyche-sync-setup remains only as the hub-mode seam.

D9-5 COMPLETE 2026-06-04


The file C:\Users\decid\Documents\projects\spt-core\M4-D9-PLAN.md has been updated successfully. (file state is current in your context — no need to Read it back)

# M4-D9 — activation sweep + cross-node E2E + M4 closeout (JIT plan)

**Status:** D9-5 COMPLETE 2026-06-04

# M4-D9 — activation sweep + cross-node E2E + M4 closeout (JIT plan)

**Status:** D9-6 COMPLETE 2026-06-04 (the two-host rig proof is REAL — CI run
`26958175812`, both roles green first attempt):
- D9-6-1 `107240e` — `tests/twohost.rs`: the §D9b ladder as `SPT_TWO_HOST=1`-gated
  tests (silent skip otherwise; identities/addresses derived from a shared secret +
  static env — no runtime exchange, both legs non-interactive); `BindScope::Port`
  (fixed dialable UDP port); `docs/TWO-HOST-RUNBOOK.md`; the `[twohost]`
  commit-message CI gate (both rig hosts are the runners; `needs: test` releases
  the roles together). Ladder proven green locally first (two processes over
  127.0.0.1; the smoke caught a real test-op/pump-op journal collision).
- D9-6-2 `a86371c` — the rig run: pair (real cross-host SPAKE2/TOTP, trust pinned
  both sides + seed transferred) → register (rows replicated both directions) →
  WAN message (spooled at B) → remote-drive (file fetch + attach echo + file push)
  → Psyche sync (both directions bootstrap-pull) → update self-heal (v6 verified +
  staged + consent-notified at A) → notif (insert on A fired on B; dismiss on B
  replicated to A). HFENDULEAM ↔ gravity-linux over tailscale; whole ladder ~8 s
  once both sides up; no firewall rule needed (the pump keeps the reverse UDP
  4-tuple warm). Evidence in the runbook appendix; `docs/FLAKE-LEDGER.md` made
  durable en route (digest flake second occurrence, rerun passed).
- int stages activated post-proof: REQ-NET-1, REQ-INST-5, REQ-INST-7, REQ-INST-8,
  REQ-REACH-1, REQ-PAIR-1, REQ-PAIR-5, REQ-NOTIF-1, REQ-UPD-1 → `[impl, unit, int]`.
- gh-repo interim RETIRED (CONTEXT.md note): the sister gh-repo-sync skill stands
  down; psyche-sync-setup remains only as the hub-mode seam.

D9-5 COMPLETE 2026-06-04

-**Status:** D9-5 COMPLETE 2026-06-04 (D9-3, D9-4, D9-5 all CI-green; REQ-INST-3 at

+**Status:** D9-6 COMPLETE 2026-06-04 (the two-host rig proof is REAL — CI run

+`26958175812`, both roles green first attempt):

+- D9-6-1 `107240e` — `tests/twohost.rs`: the §D9b ladder as `SPT_TWO_HOST=1`-gated

+  tests (silent skip otherwise; identities/addresses derived from a shared secret +

+  (fixed dialable UDP port); `docs/TWO-HOST-RUNBOOK.md`; the `[twohost]`

+  commit-message CI gate (both rig hosts are the runners; `needs: test` releases

+  the roles together). Ladder proven green locally first (two processes over

+  127.0.0.1; the smoke caught a real test-op/pump-op journal collision).

+- D9-6-2 `a86371c` — the rig run: pair (real cross-host SPAKE2/TOTP, trust pinned

+  both sides + seed transferred) → register (rows replicated both directions) →

+  WAN message (spooled at B) → remote-drive (file fetch + attach echo + file push)

+  → Psyche sync (both directions bootstrap-pull) → update self-heal (v6 verified +

+  replicated to A). HFENDULEAM ↔ gravity-linux over tailscale; whole ladder ~8 s

+  once both sides up; no firewall rule needed (the pump keeps the reverse UDP

+  4-tuple warm). Evidence in the runbook appendix; `docs/FLAKE-LEDGER.md` made

+  durable en route (digest flake second occurrence, rerun passed).

+- int stages activated post-proof: REQ-NET-1, REQ-INST-5, REQ-INST-7, REQ-INST-8,

+  REQ-REACH-1, REQ-PAIR-1, REQ-PAIR-5, REQ-NOTIF-1, REQ-UPD-1 → `[impl, unit, int]`.

+- gh-repo interim RETIRED (CONTEXT.md note): the sister gh-repo-sync skill stands

+  down; psyche-sync-setup remains only as the hub-mode seam.

+D9-5 COMPLETE 2026-06-04 (D9-3, D9-4, D9-5 all CI-green; REQ-INST-3 at

`[doc, impl, unit]`, REQ-INST-14 and REQ-INST-15 activated at `[doc, impl, unit]`):

- D9-3 `785002e`+`6f61a89`+`650c7fb` — dormancy budget measured on BOTH rig hosts

(the `[budget]` commit-message CI gate runs the env-gated harness


Bash

traceable-reqs check >/dev/null 2>&1; echo "check exit: $?"; git status --short

Gate + changed files


check exit: 0
 M CONTEXT.md
 M M4-D9-PLAN.md
 M docs/TWO-HOST-RUNBOOK.md
 M traceable-reqs.toml


Two bare-line reqs (NOTIF-1, REACH-1) — verify flipped:


grep -n -A2 'id = "REQ-NOTIF-1"\|id = "REQ-REACH-1"' traceable-reqs.toml | grep required_stages

Check NOTIF-1/REACH-1 stages


225-required_stages = ["impl", "unit"]
303-required_stages = ["impl", "unit"]


Missed — fixing both:


python - <<'EOF'
p = 'traceable-reqs.toml'
s = open(p, encoding='utf-8').read()
edits = [
  ('title = "Off-node remote-drive detection + file transfer"\nrequired_stages = ["impl", "unit"]',
   'title = "Off-node remote-drive detection + file transfer"\nrequired_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — file fetch off B + push back to B across the rig; chunked xfer through the dispatcher gate)'),
  ('title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"\nrequired_stages = ["impl", "unit"]',
   'title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"\nrequired_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — insert on A fires on B; dismiss on B replicates back to A; the update consent notif also surfaced through the same primitive)'),
]
for old, new in edits:
    assert s.count(old) == 1, f"NOT UNIQUE/FOUND: {old[:60]}"
    s = s.replace(old, new)
open(p, 'w', encoding='utf-8', newline='\n').write(s)
print("ok")
EOF
traceable-reqs check >/dev/null 2>&1; echo "check exit: $?"; grep -c '"impl", "unit", "int"' traceable-reqs.toml

Flip NOTIF-1 + REACH-1 int stages, re-check


ok
check exit: 0
26


All 9 active (17→26 int-staged reqs). Commit closeout slice:


git add traceable-reqs.toml docs/TWO-HOST-RUNBOOK.md CONTEXT.md M4-D9-PLAN.md && git commit -m "$(cat <<'EOF'
docs(reqs,plan): M4-D9-6 complete — rig proof evidence, int stages live, gh-interim retired

The two-host proof ran REAL (CI 26958175812 on a86371c, both roles green
first attempt, full ladder ~8s over tailscale): evidence captured into the
TWO-HOST-RUNBOOK appendix. Nine requirements activate their int stage on
that evidence: REQ-NET-1, REQ-INST-5, REQ-INST-7, REQ-INST-8, REQ-REACH-1,
REQ-PAIR-1, REQ-PAIR-5, REQ-NOTIF-1, REQ-UPD-1 -> [impl, unit, int].
CONTEXT.md records the gh-repo interim retirement (sister gh-repo-sync
skill stands down; psyche-sync-setup remains only as the hub-mode seam).
Plan status: D9-6 COMPLETE; D9-7 closeout sweep next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)" && git push origin dev-freeform && git rev-parse HEAD

Commit + push D9-6 closeout


warning: in the working copy of 'CONTEXT.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'M4-D9-PLAN.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/TWO-HOST-RUNBOOK.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 2ba7906] docs(reqs,plan): M4-D9-6 complete — rig proof evidence, int stages live, gh-interim retired
 4 files changed, 74 insertions(+), 11 deletions(-)
To https://github.com/SaberMage/spt-core.git
   a86371c..2ba7906  dev-freeform -> dev-freeform
2ba79061f05c5b273485fac83671cf2bdbc4681d


Write docs/TWO-HOST-RUNBOOK.md


Updated task #3 status


Updated task #4 status


until gh run list --commit 2ba79061f05c5b273485fac83671cf2bdbc4681d --json status --jq '.[0].status' 2>/dev/null | grep -q completed; do sleep 45; done; gh run list --commit 2ba79061f05c5b273485fac83671cf2bdbc4681d --json conclusion --jq '.[0] | "FINAL: \(.conclusion)"'

Watch CI FINAL on 2ba7906


Command running in background with ID: bfhyfy2z3. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bfhyfy2z3.output. You will be notified when it completes. To check interim output, use Read on that file path.


D9-6 complete. Rolling into D9-7 closeout sweep while CI watches. First: full req audit:


traceable-reqs check 2>&1 | grep -E "\[OK\]|\[FAIL\]|required: \[\]" | head -80

Full check output overview


  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-API-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-ARCH-1  required: [impl]  stages: -doc +impl -unit -int
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
  [OK] REQ-ARCH-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-ARCH-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-DAEMON-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-DAEMON-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-DAEMON-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-DAEMON-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-DOCS-1  required: []  stages: -doc -impl -unit -int
  [OK] REQ-DOCS-2  required: []  stages: -doc -impl -unit -int
  [OK] REQ-DOCS-3  required: []  stages: -doc -impl -unit -int
  [OK] REQ-DOCS-4  required: []  stages: -doc -impl -unit -int
  [OK] REQ-DOCS-5  required: []  stages: -doc -impl -unit -int
  [OK] REQ-EP-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-EP-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-EP-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-EP-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-EP-5  required: []  stages: -doc -impl -unit -int
  [OK] REQ-FRONT-1  required: []  stages: -doc -impl -unit -int
  [OK] REQ-HAZARD-CASCADE-WIPE-GUARD  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-CONFLICT-BOTH-PRESERVED  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-CONPTY-DSR  required: [impl, unit]  stages: -doc +impl +unit +int
  [OK] REQ-HAZARD-DAEMON-HOSTED-LIVENESS  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-HAZARD-DAEMON-SCHED-NONBLOCKING  required: []  stages: -doc -impl -unit -int
  [OK] REQ-HAZARD-DEFERRED-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-DEFERRED-SURVIVE-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-DIRECT-WRITE-PRECEDENCE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-DROP-FILE-SINGLE-WRITER  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-EBUSY-RENAME  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-ECHO-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-EPHEMERAL-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-EVENTPART-REASSEMBLY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-GEN-START-NOW  required: [impl, int]  stages: -doc +impl -unit +int
  [OK] REQ-HAZARD-GRACE-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-HANDOFF-ARGV-COMPAT  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-ID-CHARSET  required: [impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-HAZARD-INBOX-NO-DOUBLE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-INFO-JSON-TORN-READ  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-LOCAL-API-AUTH  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-PAIR-RATE-LIMIT  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-PAIR-SEED-ROTATION  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-PAIR-TRANSCRIPT-BIND  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-PARENT-PID-PREFER  required: []  stages: -doc -impl -unit -int
  [OK] REQ-HAZARD-PSYCHE-OUTBOUND-PROXY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-REGISTRY-CONCURRENT  required: [impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-HAZARD-REGISTRY-DIR-CREATE  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-HAZARD-REGISTRY-EPOCH-LEASE  required: [impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-HAZARD-REGISTRY-STALE-CLEAN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-RESTART-IDEMPOTENT  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-HAZARD-SINGLE-PATH-SOURCE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-SOFT-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-STALE-INDEX-LOCK  required: []  stages: -doc -impl -unit -int
  [OK] REQ-HAZARD-STALE-SIGNOFF-SENTINEL  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-STDIN-SESSION-ID  required: []  stages: -doc -impl -unit -int
  [OK] REQ-HAZARD-SUBPROCESS-TIMEOUT  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-UNC-PATH-STRIP  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-UPDATE-ROLLBACK  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-WAN-ORIGIN-AUTH  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-HAZARD-WINDOWS-PID-RECYCLE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-WORKER-PATH  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INFRA-1  required: []  stages: -doc -impl -unit -int
  [OK] REQ-INST-1  required: []  stages: -doc -impl -unit -int
  [OK] REQ-INST-10  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-11  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-12  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-13  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-14  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-INST-15  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-INST-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-3  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-INST-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INST-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-INST-6  required: []  stages: -doc -impl -unit -int
  [OK] REQ-INST-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-INST-8  required: [impl, unit, int]  stages: -doc +impl +unit +int


traceable-reqs check 2>&1 | grep -E "\[OK\]|\[FAIL\]" | tail -45; echo "==== inactive (rule-5) reqs ===="; grep -B2 'required_stages = \[\]' traceable-reqs.toml | grep '^id' | tr -d '"'

Rest of check output + inactive req list


  [OK] REQ-INSTALL-4  required: []  stages: -doc -impl -unit -int
  [OK] REQ-MANIFEST-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-MIGRATE-1  required: []  stages: -doc -impl -unit -int
  [OK] REQ-MSG-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-MSG-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-MSG-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-NET-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-NET-2  required: [impl]  stages: -doc +impl +unit -int
  [OK] REQ-NET-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-NODE-IDENTITY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-NOTIF-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-NOTIF-2  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-PAIR-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-PAIR-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-PAIR-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-PAIR-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-PAIR-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-PAIR-6  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-PAIR-7  required: []  stages: -doc -impl -unit -int
  [OK] REQ-REACH-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-REACH-2  required: []  stages: -doc -impl -unit -int
  [OK] REQ-SEAM-ACTIVITY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-CAPABILITY  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-HISTORY  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-SEAM-INJECT  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-POSTSPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-PSYCHE  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-SEAM-RESUME  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-SPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEAM-UPDATE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SEC-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-START-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-START-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-START-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-START-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-STORE-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
==== inactive (rule-5) reqs ====
id = REQ-EP-5
id = REQ-INST-1
id = REQ-INST-6
id = REQ-REACH-2
id = REQ-PAIR-7
id = REQ-FRONT-1
id = REQ-INSTALL-1
id = REQ-INSTALL-2
id = REQ-INSTALL-3
id = REQ-INSTALL-4
id = REQ-MIGRATE-1
id = REQ-INFRA-1
id = REQ-DOCS-1
id = REQ-DOCS-2
id = REQ-DOCS-3
id = REQ-DOCS-4
id = REQ-DOCS-5
id = REQ-HAZARD-STALE-INDEX-LOCK
id = REQ-HAZARD-PARENT-PID-PREFER
id = REQ-HAZARD-STDIN-SESSION-ID
id = REQ-HAZARD-DAEMON-SCHED-NONBLOCKING


All active reqs OK. Verify every inactive req carries rule-5 rationale:


grep -A2 'id = "REQ-EP-5"\|id = "REQ-INST-1"\|id = "REQ-REACH-2"\|id = "REQ-PAIR-7"\|id = "REQ-FRONT-1"\|id = "REQ-INSTALL-\|id = "REQ-MIGRATE-1"\|id = "REQ-INFRA-1"\|id = "REQ-DOCS-\|id = "REQ-HAZARD-STALE-INDEX-LOCK"\|id = "REQ-HAZARD-PARENT-PID-PREFER"\|id = "REQ-HAZARD-STDIN-SESSION-ID"\|id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"' traceable-reqs.toml | grep 'required_stages = \[\]' | sed 's/required_stages = \[\]//' | awk '{print (length($0)>5 ? "HAS-COMMENT" : "BARE")}' | sort | uniq -c

Count inactive reqs with/without rationale


     19 BARE
      1 HAS-COMMENT


grep -n -B2 'required_stages = \[\]$' traceable-reqs.toml | grep -A1 '^[0-9]*-id' | grep '^[0-9]*-id' | tr -d '"'

List bare inactive reqs with line numbers


156-id = REQ-EP-5
162-id = REQ-INST-1
227-id = REQ-REACH-2
291-id = REQ-PAIR-7
350-id = REQ-FRONT-1
356-id = REQ-INSTALL-1
360-id = REQ-INSTALL-2
364-id = REQ-INSTALL-3
368-id = REQ-INSTALL-4
372-id = REQ-MIGRATE-1
376-id = REQ-INFRA-1
382-id = REQ-DOCS-1
386-id = REQ-DOCS-2
390-id = REQ-DOCS-3
394-id = REQ-DOCS-4
398-id = REQ-DOCS-5
416-id = REQ-HAZARD-STALE-INDEX-LOCK
428-id = REQ-HAZARD-PARENT-PID-PREFER
432-id = REQ-HAZARD-STDIN-SESSION-ID


Read bare inactive req entries


[[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-REACH-2"
title = "Remote command execution (deferred, consent-gated)"
required_stages = []
[[requirements]]
id = "REQ-PAIR-7"
title = "Subnet icon (inline image metadata, GUI-only consumer)"
[[requirements]]
id = "REQ-FRONT-1"
title = "Day-one launcher/manager frontend (list/launch/attach/init)"
required_stages = []

# ───────────────────────────── Install / migration / infra ──────────────────
[[requirements]]
id = "REQ-INSTALL-1"
title = "Two install paths; signed one-line script; OS-service registration"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-2"
title = "Marketplace-repackaging-friendly install"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-3"
title = "Idempotent + interactive-optional first run"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-4"
title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"
required_stages = []
[[requirements]]
id = "REQ-MIGRATE-1"
title = "Auto-detect and migrate a legacy claude_skill_owl install"
required_stages = []
[[requirements]]
id = "REQ-INFRA-1"
title = "GitHub issue tracking for v1; tangled.org as migration target"
required_stages = []

# ───────────────────────────── Documentation ────────────────────────────────
[[requirements]]
id = "REQ-DOCS-1"
title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
required_stages = []
[[requirements]]
id = "REQ-DOCS-2"
title = "Sub-10-minute runnable killer quickstart per audience"
required_stages = []
[[requirements]]
id = "REQ-DOCS-3"
title = "Diátaxis structure; one canonical way to do X"
required_stages = []
[[requirements]]
id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = []
[[requirements]]
id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = []
required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
[[requirements]]
id = "REQ-HAZARD-STALE-INDEX-LOCK"
title = "Sweep stale lockfiles on daemon boot (1.3)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DEFERRED-DRAIN"
title = "Deferred spool rows excluded from the event-stream drain (1.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
[[requirements]]
id = "REQ-HAZARD-WORKER-PATH"
title = "Single source of truth for Worker/Psyche perch location (1.5)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-PARENT-PID-PREFER"
title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-STDIN-SESSION-ID"
title = "Stdin session_id precedence over env (2.2)"
required_stages = []


python - <<'EOF'
p = 'traceable-reqs.toml'
s = open(p, encoding='utf-8').read()

# Rule-5 audit (D9-7): every inactive req gets an explicit deferral rationale.
rationales = {
  'REQ-EP-5': 'rule 5: M5 — shell instantiation rides the per-agent-runtime/daemon-hosted-shells work; seams shaped in D4/D5/D8 (M4-PLAN §Out)',
  'REQ-INST-1': 'rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates at M5 when instantiate-anywhere makes the split user-facing (instance minting); its concrete slices are tracked by REQ-INST-7..15',
  'REQ-REACH-2': 'rule 5: deferred by design (PRD: consent-gated remote exec is the highest-risk capability; workarounds exist) — M5+ with the instantiate-anywhere consent model',
  'REQ-PAIR-7': 'rule 5: GUI milestone (user 2026-06-04) — inline image metadata has no consumer until a GUI exists',
  'REQ-FRONT-1': 'rule 5: GUI milestone — no frontend work in M0–M4',
  'REQ-INSTALL-1': 'rule 5: install/packaging milestone (post-M4) — no installer work yet',
  'REQ-INSTALL-2': 'rule 5: install/packaging milestone (post-M4)',
  'REQ-INSTALL-3': 'rule 5: install/packaging milestone (post-M4)',
  'REQ-INSTALL-4': 'rule 5: M5 — adapter registration lifecycle lands with the adapter-hosting work (manifest seams already shipped, REQ-SEAM-*)',
  'REQ-MIGRATE-1': 'rule 5: post-v1 — migration tooling needs a v1 to migrate to',
  'REQ-INFRA-1': 'rule 5: process req, not code — tracked operationally; activates if/when a docs/infra milestone formalizes it',
  'REQ-DOCS-1': 'rule 5: docs milestone (post-M4) — DOCS-STRATEGY.md exists; the doc *product* reqs activate when docs become a deliverable',
  'REQ-DOCS-2': 'rule 5: docs milestone (post-M4)',
  'REQ-DOCS-3': 'rule 5: docs milestone (post-M4)',
  'REQ-DOCS-4': 'rule 5: docs milestone (post-M4)',
  'REQ-DOCS-5': 'rule 5: docs milestone (post-M4)',
  'REQ-HAZARD-STALE-INDEX-LOCK': 'rule 5: binds when the daemon owns git-index-touching sweeps at boot — the BranchStore git runner (gitrun.rs) is bounded per ADR-0013 and no boot sweep exists yet (M5 daemon-hosted shells/runtimes)',
  'REQ-HAZARD-PARENT-PID-PREFER': 'rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)',
  'REQ-HAZARD-STDIN-SESSION-ID': 'rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such adapter path exists in spt-core yet (M5 adapter work)',
}
count = 0
for req, why in rationales.items():
    marker = f'id = "{req}"'
    i = s.index(marker)
    j = s.index('required_stages = []', i)
    line_end = s.index('\n', j)
    line = s[j:line_end]
    if line.strip() == 'required_stages = []':
        s = s[:j] + f'required_stages = []  # {why}' + s[line_end:]
        count += 1
open(p, 'w', encoding='utf-8', newline='\n').write(s)
print("annotated", count)
EOF
traceable-reqs check >/dev/null 2>&1; echo "check exit: $?"

Annotate all bare inactive reqs with rule-5 rationale


annotated 19
check exit: 0


19 annotated, check green. Next: `apply_brain_only` no-production-caller note:


crates\spt-daemon\tests\brain_swap.rs:6://! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)
crates\spt-daemon\tests\brain_swap.rs:32:use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};
crates\spt-daemon\tests\brain_swap.rs:114:    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))
crates\spt-daemon\src\lib.rs:45://! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
crates\spt-daemon\src\lib.rs:57://! every check, so an unverified binary can never reach [`apply_brain_only`].
crates\spt-daemon\src\lib.rs:193:    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
crates\spt-daemon\src\update.rs:22://! ([`apply_brain_only`]). The apply *triggers a handoff* over the existing M3b
crates\spt-daemon\src\update.rs:147:/// is produced** — there is nothing for [`apply_brain_only`] to act on, so an
crates\spt-daemon\src\update.rs:182:                write!(f, "apply_brain_only refused a {c:?} plan (would risk an endpoint)")
crates\spt-daemon\src\update.rs:214:pub fn apply_brain_only<F>(
crates\spt-daemon\src\update.rs:340:    // is nothing for apply_brain_only to act on, so an unverified binary can never


195	    }
196	}
197
198	/// Apply a **brain-only** update ([`REQ-UPD-3`]): swap the brain logic without
199	/// disturbing any broker-held resource or hosted endpoint.
200	///
201	/// This is a deliberate trigger of the M3b handoff path, **not** a new handoff:
202	/// it snapshots the outgoing brain ([`Brain::snapshot`]), drops it — the exact
203	/// crash boundary the broker already tolerates (the subscriber vanishes; the
204	/// child + its buffered output persist) — and re-attaches by calling `relaunch`
205	/// with the snapshot. `relaunch` stands for "exec the new binary's brain, which
206	/// re-attaches via [`Brain::handoff`]"; in the live daemon it execs the verified
207	/// new binary, and the returned brain is generation+1 with a fresh `gen_start`.
208	///
209	/// The guard is the invariant: a non-brain-only `plan` is refused
210	/// ([`ApplyError::NotBrainOnly`]) **before** the outgoing brain is dropped, so
211	/// the brain-only path can never be the thing that cycles an endpoint. Broker
212	/// touching classes route elsewhere (C2 consent + scheduling).
213	// [impl->REQ-UPD-3]
214	pub fn apply_brain_only<F>(
215	    plan: &UpdatePlan,
216	    outgoing: Brain,
217	    relaunch: F,
218	) -> Result<Brain, ApplyError>
219	where
220	    F: FnOnce(BrainState) -> io::Result<Brain>,
221	{
222	    if plan.class != UpdateClass::BrainOnly {
223	        // Refuse *before* touching the outgoing brain — no endpoint is at risk.
224	        return Err(ApplyError::NotBrainOnly(plan.class));
225	    }
226	    let state = outgoing.snapshot();
227	    drop(outgoing); // the crash boundary the broker tolerates: child + log persist
228	    // The new binary's brain re-attaches over the existing substrate. A relaunch
229	    // failure surfaces to the caller, which may retry against the still-live
230	    // broker (the endpoint is untouched regardless — that is the whole point).
231	    relaunch(state).map_err(ApplyError::Relaunch)
232	}
233
234	#[cfg(test)]

    }
}

/// Apply a **brain-only** update ([`REQ-UPD-3`]): swap the brain logic without
/// disturbing any broker-held resource or hosted endpoint.
///
/// This is a deliberate trigger of the M3b handoff path, **not** a new handoff:
/// it snapshots the outgoing brain ([`Brain::snapshot`]), drops it — the exact
/// crash boundary the broker already tolerates (the subscriber vanishes; the
/// child + its buffered output persist) — and re-attaches by calling `relaunch`
/// with the snapshot. `relaunch` stands for "exec the new binary's brain, which
/// re-attaches via [`Brain::handoff`]"; in the live daemon it execs the verified
/// new binary, and the returned brain is generation+1 with a fresh `gen_start`.
///
/// The guard is the invariant: a non-brain-only `plan` is refused
/// ([`ApplyError::NotBrainOnly`]) **before** the outgoing brain is dropped, so
/// the brain-only path can never be the thing that cycles an endpoint. Broker
/// touching classes route elsewhere (C2 consent + scheduling).
// [impl->REQ-UPD-3]
pub fn apply_brain_only<F>(
    plan: &UpdatePlan,
    outgoing: Brain,
    relaunch: F,
) -> Result<Brain, ApplyError>
where
    F: FnOnce(BrainState) -> io::Result<Brain>,
{
    if plan.class != UpdateClass::BrainOnly {
        // Refuse *before* touching the outgoing brain — no endpoint is at risk.
        return Err(ApplyError::NotBrainOnly(plan.class));
    }
    let state = outgoing.snapshot();
    drop(outgoing); // the crash boundary the broker tolerates: child + log persist
    // The new binary's brain re-attaches over the existing substrate. A relaunch
    // failure surfaces to the caller, which may retry against the still-live
    // broker (the endpoint is untouched regardless — that is the whole point).
    relaunch(state).map_err(ApplyError::Relaunch)
}

#[cfg(test)]


Edit

/// The guard is the invariant: a non-brain-only `plan` is refused
/// ([`ApplyError::NotBrainOnly`]) **before** the outgoing brain is dropped, so
/// the brain-only path can never be the thing that cycles an endpoint. Broker
/// touching classes route elsewhere (C2 consent + scheduling).
// [impl->REQ-UPD-3]

/// The guard is the invariant: a non-brain-only `plan` is refused
/// ([`ApplyError::NotBrainOnly`]) **before** the outgoing brain is dropped, so
/// the brain-only path can never be the thing that cycles an endpoint. Broker
/// touching classes route elsewhere (C2 consent + scheduling).
///
/// **No production caller yet (M4-D9 audit, honest residual):** the peer pump's
/// update leg stops at *staged + consent-notified* (REQ-UPD-4 gated default) —
/// nothing in the daemon invokes this apply after the user acks the consent
/// notif. Binary APPLY orchestration after a staged+consented update is the
/// M3c machinery's seam (`brain_swap.rs` proves the swap end-to-end through
/// the real handoff; the wiring from notif-ack → apply lands with the M5
/// daemon-lifecycle work).
// [impl->REQ-UPD-3]


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\update.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// The guard is the invariant: a non-brain-only `plan` is refused
/// ([`ApplyError::NotBrainOnly`]) **before** the outgoing brain is dropped, so
/// the brain-only path can never be the thing that cycles an endpoint. Broker
/// touching classes route elsewhere (C2 consent + scheduling).
// [impl->REQ-UPD-3]

/// The guard is the invariant: a non-brain-only `plan` is refused
/// ([`ApplyError::NotBrainOnly`]) **before** the outgoing brain is dropped, so
/// the brain-only path can never be the thing that cycles an endpoint. Broker
/// touching classes route elsewhere (C2 consent + scheduling).
///
/// **No production caller yet (M4-D9 audit, honest residual):** the peer pump's
/// update leg stops at *staged + consent-notified* (REQ-UPD-4 gated default) —
/// nothing in the daemon invokes this apply after the user acks the consent
/// notif. Binary APPLY orchestration after a staged+consented update is the
/// M3c machinery's seam (`brain_swap.rs` proves the swap end-to-end through
/// the real handoff; the wiring from notif-ack → apply lands with the M5
/// daemon-lifecycle work).
// [impl->REQ-UPD-3]

+/// **No production caller yet (M4-D9 audit, honest residual):** the peer pump's

+/// update leg stops at *staged + consent-notified* (REQ-UPD-4 gated default) —

+/// nothing in the daemon invokes this apply after the user acks the consent

+/// notif. Binary APPLY orchestration after a staged+consented update is the

+/// M3c machinery's seam (`brain_swap.rs` proves the swap end-to-end through

+/// the real handoff; the wiring from notif-ack → apply lands with the M5


ROADMAP + CONTEXT glossary next. Check what D9 amendments CONTEXT already has:


Grep

fork|resource advertisement|auto-suspend|dormancy budget|warm default


94:[Omitted long matching line]
285:[Omitted long matching line]
378:- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).
383:- **dormant → suspended** — manual (`spt suspend` / `spt shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
388:[Omitted long matching line]
394:**`spt fork <id> [--into <subnet>] [--as <new_id>] [--delete-source]`** (cross-subnet clone):
395:[Omitted long matching line]
415:[Omitted long matching line]
418:**resource advertisement (subnet resource registry)**:
525:[Omitted long matching line]


Bash

Read resting/fork/resources glossary entries


**dormant / suspended instance:**
At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).

**State transitions (no idle timer):**
- **active** = the instance is the **most-recently-interacted instance for that ID** *and* **has a driver attached**. Both conditions. There is **no idle-grace timer** — a session stays active as long as it holds those two properties. A **linked Shell counts as a driver** (an agent with a live shell stays active even when the user isn't typing).
- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
- **dormant → suspended** — manual (`spt suspend` / `spt shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
- **wake (→ active)** — `spt wake`, a driver attaches, or a shell wake-watcher fires.

A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).

**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override. *Confirmed by measurement (M4-D9-3, `docs/DORMANCY-BUDGET.md`): an idle warm seat burns zero CPU — the cost is RSS only (~8 MiB shell-class, ~300 MiB LLM-harness-class); suspended residual is just the on-disk record.*

**Commands** (user/agent-initiated lifecycle, unprefixed): `spt suspend <id[@node]>` (suspend any of your instances from any node), `spt wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).

**`spt rename <id> <new_id>`** (endpoint rename, **rippled**): change an endpoint's logical ID and propagate the change to **all its instances** subnet-wide — the registry entries, every node's perch, and the synced context branches (`a-<id>` → `a-<new_id>`, and project-view references). The rename is one logical operation against the eventually-consistent registry; it must collision-check `new_id` against every subnet the endpoint is advertised into (the join-time check above), and reconcile concurrent renames by the same precedence marker the context merge uses (node + newest-wins, KNOWN-HAZARDS 6.5). The adapter-agnostic, node-anchored identity (ADR-0003) is what makes a clean rename possible; without it the ID would be entangled with per-adapter or per-session state.

**`spt fork <id> [--into <subnet>] [--as <new_id>] [--delete-source]`** (cross-subnet clone):
Clone an endpoint into a **new, identity-distinct endpoint** on another subnet — the sanctioned way to place an agent's mind in a different subnet (home subnet being immutable). The fork's **home = the target subnet**; it is **seeded with a one-time copy** of the source's mind (the live + project *Cross-node Psyche sync* tiers), after which the two **diverge** — there is **no ongoing sync**. A fork is therefore **not an instance**: an *instance* is the **same** identity on another node with a **synced** mind; a *fork* is a **new** identity with a **copied-then-independent** mind. The new id defaults to the source's bare name if free in the target subnet (the join-time collision check; else prompt/rename); the adapter follows the normal creation rule (*adapter selection*). The **source is untouched**, optionally deleted afterward (`--delete-source`, or a later delete). Distinct from **rename** (same identity, new label, rippled) and **instantiate-anywhere** (same identity, new *node*, consent-gated).
The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).

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

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


CONTEXT glossary already amended through D9-3/4/5 ✔. ROADMAP M4 status:


36:[Omitted long matching line]
37:  - **M3 COMPLETE** (M3a + M3b + M3c all ✅, 2026-06-03): the real ADR-0004 architecture is live — native PTY hosting, the consolidated broker/brain daemon, and seamless signed self-update. Next milestone: M4.
38:- **M4 — Networking + instances** (NEXT). `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub) + the **update peer-propagation transport** plugged into the M3c engine seam (REQ-UPD-1). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
53:- ✅ **Spike #2 (Iroh smoke test)** — PASS (single-host). `docs/spikes/SPIKE-02-iroh-smoke.md`. iroh 0.98 API workable; 13.57 MB release. Cross-NAT/relay proof deferred to M4.


28	- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
29	- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
30	  - **M2a — Harness contract. ✅ delivered (2026-06-01).** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface (seed/listen/bind/state/echo-gate/poll/worker-*/boundary/session-end/presence/history-log/emit/capability) + the **harness-hosted** startup topology (seed→listen→bind, interim seed-file) + local-api-auth + the generic mock adapter + contract E2E. All M2a reqs activated + green. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
31	  - **M2b — Live-agent lifecycle. ✅ delivered (2026-06-01).** `spt-live` (fifth layer): history subsystem (fetcher/locate-normalize/native) + spawn-psyche (nested perch, spt-core-owned `$psyche_prompt`) + echo-commune (history brief → single-writer commune drop) + drop-file ingest (single-writer 6.4 + direct-write precedence 6.5) + interim pulse + signoff/boundary ordering invariants (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-signoff sweep 3.2) + resume seam (continue-existing / fresh-with-preload). Hosted by the `api listen` runner + `api shutdown`; mock adapter gained the lifecycle templates; LiveAgent E2E proves spawn→Psyche-perch→commune-ingest→graceful-signoff harness-agnostically. All M2b reqs activated + green. Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).
32	  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
33	- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update. Split M3a/M3b/M3c. Scope + carried-forward Spike #1 gaps: [`M3-PLAN.md`](./M3-PLAN.md).
34	  - **M3a — `spt-term` session-surface mechanism. ✅ delivered (2026-06-02).** The sixth crate (`…→spt-msg→spt-term`, sibling of spt-runtime): the `SessionSurface` trait + native PTY backends (ConPTY/forkpty via `portable-pty`, REQ-TERM-1) · ConPTY-DSR auto-answer drain pump (REQ-HAZARD-CONPTY-DSR) · send-keys/send-line injection (REQ-TERM-2) · bounded byte-stream (REQ-TERM-3) · the PTY-digest parser primitive (REQ-TERM-4 impl/unit, ADR-0008). OS-neutral per Spike #4/#5 (no OS's stream contract baked in; ANSI-stripped, tail-wins repaint tolerance). Validated on Windows (ConPTY) + Linux (forkpty). All M3a reqs activated + green. Task breakdown: [`M3a-PLAN.md`](./M3a-PLAN.md).
35	  - **M3b — `spt-daemon` broker/brain process. ✅ delivered (2026-06-03).** The seventh crate (`…→spt-live→spt-daemon→spt`) hosts many `spt-term` surfaces behind a **versioned local IPC** (named-pipe/UDS behind one `DaemonTransport` trait, forward-compatible frame schema): a **broker** (stable kernel — PTY masters, harness children, sockets; no logic) + a **brain** (restartable logic) so a brain kill/restart leaves the hosted PTY child + its output stream **intact and gapless** (Spike #1 made real; `gen_start = now()` each handoff). Consolidates the interim no-daemon model — the `api listen` pulse/Psyche/drop-ingest loops fold into the brain as config-paced in-process loops (only a thin stateless relay residue) · daemon-authoritative liveness (`info.json` `status` supersedes `is_process_alive` for hosted perches) · exactly-once broker↔brain boundary (broker-owned `EffectJournal` + dedup-at-effect, Spike #6) · auto-start + in-memory seed (the seed file is dead) · supervised-crash orphan-watch (carries the M2b graceful ordering) · the **digest daemon-half** (manifest `pty_digest` seam → `DigestParser` over the broker PTY, `spt digest <id>` pull + delta-stream + opt-in Path-B persistence → REQ-TERM-4 `int`). All M3b reqs activated + green; daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) on the CI matrix (ubuntu + windows). Task breakdown: [`M3b-PLAN.md`](./M3b-PLAN.md).
36	  - **M3c — signed self-update. ✅ delivered (2026-06-03).** The gated, signed, ripple-capable self-update engine (no new crate — it lives in `spt-daemon`, signing primitives in `release.rs`): the **update-class taxonomy** (brain-only / broker-compatible / broker-breaking, ADR-0004 §A) with the **brain-only zero-interruption swap** that drives the M3b handoff substrate (snapshot→drop→re-attach) so a live PTY child + its output stream survive the logic swap **untouched** (REQ-UPD-3, proven E2E `tests/brain_swap.rs`) · **verify-before-handoff** (Ed25519 over signed release metadata + SHA-256 artifact binding; `plan_verified` is the front door — no unverified binary reaches a handoff, REQ-UPD-2) · **rollback hardening** (monotonic version, metadata expiry, channel pinning, key revocation — REQ-HAZARD-UPDATE-ROLLBACK) · **consent gating** (default-gated, delivered to the most-recently-active live session via the new `info.json` `last_active_ms` recency stamp; opt-in full-auto — REQ-UPD-4) · **adapter ripple-update** (self-then-adapters via each manifest `[update]` avenue, with adapter content signing against the adapter's own key for `file_pull` / a `self_verifies` attest for opaque `delegated` updaters — REQ-UPD-5, REQ-SEAM-UPDATE). **Peer-propagation transport (REQ-UPD-1) defers to M4** — the engine ships with self-fetch/out-of-band delivery into the same seam. All M3c reqs activated + green. Task breakdown: [`M3c-PLAN.md`](./M3c-PLAN.md).
37	  - **M3 COMPLETE** (M3a + M3b + M3c all ✅, 2026-06-03): the real ADR-0004 architecture is live — native PTY hosting, the consolidated broker/brain daemon, and seamless signed self-update. Next milestone: M4.
38	- **M4 — Networking + instances** (NEXT). `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub) + the **update peer-propagation transport** plugged into the M3c engine seam (REQ-UPD-1). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
39	- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.
40
41	## De-risk spikes (do early — these can invalidate the architecture)
42
43	1. **Broker/brain no-terminate handoff** — prove a PTY child + open socket survive a daemon-*logic* restart (FD-passing or stable-broker shim). The hardest invariant in the design (ADR-0004).
44	2. **Iroh + mDNS smoke test** — two nodes, separate NATs, one message end-to-end; measure direct-vs-relay + binary size (research brief §5). Validates the whole networking premise.
45	3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
46	4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.
47
48	## Immediate next steps
49
50	Progress (2026-05-31):
51	- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
52	- ✅ **Spike #1 (broker/brain handoff)** — PASS on Windows ConPTY. `docs/spikes/SPIKE-01-broker-handoff.md`. Found+fixed the ConPTY DSR-stall hazard. 4 open gaps carried to M3.
53	- ✅ **Spike #2 (Iroh smoke test)** — PASS (single-host). `docs/spikes/SPIKE-02-iroh-smoke.md`. iroh 0.98 API workable; 13.57 MB release. Cross-NAT/relay proof deferred to M4.
54	- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).
55
56	Remaining before M0:
57	1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).

- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
  - **M2a — Harness contract. ✅ delivered (2026-06-01).** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface (seed/listen/bind/state/echo-gate/poll/worker-*/boundary/session-end/presence/history-log/emit/capability) + the **harness-hosted** startup topology (seed→listen→bind, interim seed-file) + local-api-auth + the generic mock adapter + contract E2E. All M2a reqs activated + green. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
  - **M2b — Live-agent lifecycle. ✅ delivered (2026-06-01).** `spt-live` (fifth layer): history subsystem (fetcher/locate-normalize/native) + spawn-psyche (nested perch, spt-core-owned `$psyche_prompt`) + echo-commune (history brief → single-writer commune drop) + drop-file ingest (single-writer 6.4 + direct-write precedence 6.5) + interim pulse + signoff/boundary ordering invariants (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-signoff sweep 3.2) + resume seam (continue-existing / fresh-with-preload). Hosted by the `api listen` runner + `api shutdown`; mock adapter gained the lifecycle templates; LiveAgent E2E proves spawn→Psyche-perch→commune-ingest→graceful-signoff harness-agnostically. All M2b reqs activated + green. Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).
  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update. Split M3a/M3b/M3c. Scope + carried-forward Spike #1 gaps: [`M3-PLAN.md`](./M3-PLAN.md).
  - **M3a — `spt-term` session-surface mechanism. ✅ delivered (2026-06-02).** The sixth crate (`…→spt-msg→spt-term`, sibling of spt-runtime): the `SessionSurface` trait + native PTY backends (ConPTY/forkpty via `portable-pty`, REQ-TERM-1) · ConPTY-DSR auto-answer drain pump (REQ-HAZARD-CONPTY-DSR) · send-keys/send-line injection (REQ-TERM-2) · bounded byte-stream (REQ-TERM-3) · the PTY-digest parser primitive (REQ-TERM-4 impl/unit, ADR-0008). OS-neutral per Spike #4/#5 (no OS's stream contract baked in; ANSI-stripped, tail-wins repaint tolerance). Validated on Windows (ConPTY) + Linux (forkpty). All M3a reqs activated + green. Task breakdown: [`M3a-PLAN.md`](./M3a-PLAN.md).
  - **M3b — `spt-daemon` broker/brain process. ✅ delivered (2026-06-03).** The seventh crate (`…→spt-live→spt-daemon→spt`) hosts many `spt-term` surfaces behind a **versioned local IPC** (named-pipe/UDS behind one `DaemonTransport` trait, forward-compatible frame schema): a **broker** (stable kernel — PTY masters, harness children, sockets; no logic) + a **brain** (restartable logic) so a brain kill/restart leaves the hosted PTY child + its output stream **intact and gapless** (Spike #1 made real; `gen_start = now()` each handoff). Consolidates the interim no-daemon model — the `api listen` pulse/Psyche/drop-ingest loops fold into the brain as config-paced in-process loops (only a thin stateless relay residue) · daemon-authoritative liveness (`info.json` `status` supersedes `is_process_alive` for hosted perches) · exactly-once broker↔brain boundary (broker-owned `EffectJournal` + dedup-at-effect, Spike #6) · auto-start + in-memory seed (the seed file is dead) · supervised-crash orphan-watch (carries the M2b graceful ordering) · the **digest daemon-half** (manifest `pty_digest` seam → `DigestParser` over the broker PTY, `spt digest <id>` pull + delta-stream + opt-in Path-B persistence → REQ-TERM-4 `int`). All M3b reqs activated + green; daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) on the CI matrix (ubuntu + windows). Task breakdown: [`M3b-PLAN.md`](./M3b-PLAN.md).
  - **M3c — signed self-update. ✅ delivered (2026-06-03).** The gated, signed, ripple-capable self-update engine (no new crate — it lives in `spt-daemon`, signing primitives in `release.rs`): the **update-class taxonomy** (brain-only / broker-compatible / broker-breaking, ADR-0004 §A) with the **brain-only zero-interruption swap** that drives the M3b handoff substrate (snapshot→drop→re-attach) so a live PTY child + its output stream survive the logic swap **untouched** (REQ-UPD-3, proven E2E `tests/brain_swap.rs`) · **verify-before-handoff** (Ed25519 over signed release metadata + SHA-256 artifact binding; `plan_verified` is the front door — no unverified binary reaches a handoff, REQ-UPD-2) · **rollback hardening** (monotonic version, metadata expiry, channel pinning, key revocation — REQ-HAZARD-UPDATE-ROLLBACK) · **consent gating** (default-gated, delivered to the most-recently-active live session via the new `info.json` `last_active_ms` recency stamp; opt-in full-auto — REQ-UPD-4) · **adapter ripple-update** (self-then-adapters via each manifest `[update]` avenue, with adapter content signing against the adapter's own key for `file_pull` / a `self_verifies` attest for opaque `delegated` updaters — REQ-UPD-5, REQ-SEAM-UPDATE). **Peer-propagation transport (REQ-UPD-1) defers to M4** — the engine ships with self-fetch/out-of-band delivery into the same seam. All M3c reqs activated + green. Task breakdown: [`M3c-PLAN.md`](./M3c-PLAN.md).
  - **M3 COMPLETE** (M3a + M3b + M3c all ✅, 2026-06-03): the real ADR-0004 architecture is live — native PTY hosting, the consolidated broker/brain daemon, and seamless signed self-update. Next milestone: M4.
- **M4 — Networking + instances** (NEXT). `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub) + the **update peer-propagation transport** plugged into the M3c engine seam (REQ-UPD-1). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.

## De-risk spikes (do early — these can invalidate the architecture)

1. **Broker/brain no-terminate handoff** — prove a PTY child + open socket survive a daemon-*logic* restart (FD-passing or stable-broker shim). The hardest invariant in the design (ADR-0004).
2. **Iroh + mDNS smoke test** — two nodes, separate NATs, one message end-to-end; measure direct-vs-relay + binary size (research brief §5). Validates the whole networking premise.
3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.

## Immediate next steps

Progress (2026-05-31):
- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
- ✅ **Spike #1 (broker/brain handoff)** — PASS on Windows ConPTY. `docs/spikes/SPIKE-01-broker-handoff.md`. Found+fixed the ConPTY DSR-stall hazard. 4 open gaps carried to M3.
- ✅ **Spike #2 (Iroh smoke test)** — PASS (single-host). `docs/spikes/SPIKE-02-iroh-smoke.md`. iroh 0.98 API workable; 13.57 MB release. Cross-NAT/relay proof deferred to M4.
- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).

Remaining before M0:
1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).


Edit

- **M4 — Networking + instances** (NEXT). `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub) + the **update peer-propagation transport** plugged into the M3c engine seam (REQ-UPD-1). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.

- **M4 — Networking + instances. ✅ delivered (2026-06-04).** The eighth crate `spt-net` (Iroh WAN endpoint bound to the node's own Ed25519 identity, mDNS LAN discovery, TOTP-SPAKE2 pairing over a dedicated pre-trust ALPN with transcript binding / rate limiting / seed rotation+transfer, subnet registry + NDJSON replication under the per-node epoch lease) · the multi-instance model (registry rows + bare-id resolution refuse-and-qualify + visibility/sync-membership gates + rename/collision + **resting-state machine** dormant/suspended with measured warm/cold policy [`docs/DORMANCY-BUDGET.md`, red-team #9 closed] + **resource advertisement** REQ-INST-14 + **immutable home subnet & `spt fork`** REQ-INST-15, ADR-0010) · **cross-node Psyche sync** (git-native bundles over broker QUIC streams, two-tier scope gates, vector-precedence merge with conflicts surfaced + Psyche-reconciled, ADR-0013; **the gh-repo interim is retired**) · WAN messaging + remote-drive attach + chunked file transfer + per-endpoint access whitelists · the subnet **notification primitive** (replicated spool, dismiss-replication, boundary resurfacing; update consent + escalations refactored as producers) · the **update peer-propagation transport** into the M3c engine seam (offer-then-fetch, verify-before-stage per node, consent-notified — REQ-UPD-1) · production trigger loops (peer pump + inbound dispatcher — the subnet self-drives). **The real proof ran on the real rig: `[twohost]` CI run `26958175812` — pair → register → cross-node message → remote-drive (A typed into B's live PTY and read the echo) → Psyche sync both directions → update self-heal → notif dismiss-replication, HFENDULEAM (Windows) ↔ gravity-linux, whole ladder ~8 s** (`docs/TWO-HOST-RUNBOOK.md` + evidence appendix; `tests/twohost.rs` carries the nine activated `int` stages). Task breakdowns: [`M4-PLAN.md`](./M4-PLAN.md) + per-task JIT plans (D1–D9).
- **M5 — Shells, presence, deferred capabilities** (NEXT). Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam. Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, remote fork arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.