# M4-D9 — activation sweep + cross-node E2E + M4 closeout (JIT plan)

**Status: COMPLETE 2026-06-04.** D9-7 closeout sweep done: full traceable-reqs
audit (every M4 req satisfied or rule-5-deferred with an explicit rationale
comment — 19 inactive reqs annotated; `apply_brain_only` no-production-caller
residual noted in update.rs), ROADMAP M4 delivered, CONTEXT.md glossary current
(resting policy numbers D9-3, fork/home D9-5, resource advertisement D9-4,
gh-interim retirement D9-6), CI matrix green.

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 (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
  `tests/budget.rs` on both runners): HFENDULEAM shell seat 7.6 MiB / claude seat
  ~308 MiB / zero idle CPU; gravity shell seat 1.7 MiB; suspended residual = the
  on-disk record. `docs/DORMANCY-BUDGET.md` locks the policy (warm default
  CONFIRMED; auto-suspend opt-in default OFF on the D9-2 knob chain; constrained
  nodes default the node leg ON); ADR-0003 red-team #9 CLOSED.
- D9-4 `0ea318b` — resource advertisement: `resources: Option<String>` on the
  registry `Instance` (additive, rides the lease + `RegistryUpdate` untouched),
  `resource_projection` over visible+routable rows (the exclusion closure
  resolution uses), both-authored sourcing (`info.json` via `spt resources set`
  wins; `daemon.json` `resources_blurb` node seed), `spt resources set/show/list`.
- D9-5 `e69f59a`+`0b30dea` — immutable home subnet (`spt_store::home`:
  auto-if-one / ask-if-many / local-only-until-first-join; ONE shared creation
  seam for `spt ready` + `api listen`/`bind` with carry-forward across re-binds;
  first-join adoption via the CLI create path + the advertisement-scan sweep; NO
  setter) + `sync_subnets = [home]` creation seeding + `spt fork` (one-time copy
  of both tiers as parentless seed commits — copied-then-diverged; refusal-first
  collision gates; `--delete-source` removes exactly the source; same-node v1,
  remote arm = M5).

D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and
REQ-INST-4 activated at `[impl, unit]`):
- D9-2-1 `0315783` — the explicit resting-state machine (`spt-daemon::resting`: pure
  transition table, dormancy-onset auto-suspend anchor, global→node→endpoint knob
  chain default OFF) + `info.json` `rest_state`/`dormant_since_ms` durable pair +
  registry `Status::Suspended` (additive, addressable) + `advertise_local` follows
  the machine epoch-bumped.
- D9-2-2 `8a7afd6` — edge effects: `apply_event` transition host (echo BEFORE the
  flip persists, KH 3.3; loud echo failure never wedges the seat active);
  `BrainLifecycle::rest_event` binds the real `run_echo_commune`; wake fires
  `resurface_at_boundary` (the D8 deferral closed) + the freshness-pull marker the
  peer pump consumes (forces one sync round NOW).
- D9-2-3 `56a98db` — production feeds: attach arm (Request=Wake, viewport
  end=Detach), registry arm attention flips (`apply_feed_flips` — only OBSERVED
  transitions-to-Active on peer nodes; never restart re-learns or periodic
  re-advertisements), pulse recency gated on the resting state (the D8 gap: a
  resting seat never wins most-recently-active), `spt suspend`/`spt wake` local
  arms. Daemon-side echoes are GATE-ARMED (the endpoint's own pulse loop runs the
  bounded call — KH 7.4 holds by construction; the hazard requirement stays
  unbound until the daemon hosts per-agent runtimes). REQ-INST-6 seam documented,
  stays inactive (M5). Remote `spt suspend id@node` arm = M5 seam.

D9-1 COMPLETE 2026-06-04 (five CI-green sub-slices — the slice-1 body grew
attach/xfer arms and split for atomicity):
- D9-1-1 `ea73c9a` — inbound dispatch (sync/update/notif/wan arms; `initiated_locally`
  on the stream table; first-record shape demux).
- D9-1-2 `822d3ce` — RegistryHost (gated inbound merge + owlery advertisement + atomic
  snapshots) + peer pump (registry/notif push, anti-entropy sync pull with
  registry-derived bootstrap refs) + `daemon.json` knobs + `addr_for_node_hex`.
- D9-1-3 `b15cf2c` — production VerifyPolicy (`release-keys.json`, fail-closed) +
  update pump leg (stage + consent notif, REQ-UPD-4 gated default).
- D9-1-4 `af7f394` — `spt send` WAN leg (snapshot-resolved, refuse-and-qualify;
  deferred stays local-only, documented seam).
- D9-1-5 `0dd85d5` — attach/xfer dispatch arms: `SpawnReq.endpoint` label + broker
  `sessions` IPC = the attach gate subject (table-resolved, never wire); xfer wire
  names the gating endpoint (rule set wire-named, origin handshake-proven).
Notes for later slices: the in-pump Psyche reconcile turn has no daemon-hosted
per-agent runtime to ride (the 7.4 fan-out seam) — conflicts surface loud via
`PumpHooks::on_pull`, the turn stays at activation paths; binary APPLY orchestration
after a staged+consented update remains the M3c machinery's seam (not pump scope).

**Drafted** 2026-06-04 at D9 start. Design forks resolved with user 2026-06-04:
(1) **two-host posture** = env-gated int tests + manual runbook on the real rig — no
cross-runner CI choreography (a cross-runner job is a documented M5 seam);
(2) **REQ-INST-14 and REQ-INST-15 both activate and build in D9** (the full V1-mid line);
(3) **REQ-PAIR-7** (subnet icon, GUI-only consumer) defers to the GUI milestone —
`required_stages` stays `[]`, documented seam.
Authored against M4-PLAN §D9, ADR-0003 (red-team #9), ADR-0010 (home subnet / fork),
ADR-0013 (sync + reconcile), and the deferred-to-D9 ledger accumulated across D5–D8.

## Goal

M4 exits real: every production trigger loop deferred from D5/D6/D7/D8 wired into the
daemon lifecycle (the subnet *self-drives* — sync, update, notif, reconcile fire on
their own, not only under test harnesses); the dormant/suspended resting model built and
**budgeted on real adapters** (red-team #9 closed with numbers); resource advertisement
+ immutable-home/fork shipped (REQ-INST-3, -4, -14, -15); the two-host proof demoed on
the rig (HFENDULEAM ↔ gravity-linux) with `[int->REQ-*]` evidence; the gh-repo interim
retired; every M4 req's `required_stages` satisfied; CI matrix green.

## The deferred-to-D9 ledger (all land here)

| Item | Deferred at | Lands in |
|------|-------------|----------|
| Production trigger loops (sync notify/anti-entropy, update check, notif feed pump, post-sync reconcile election) | D5/D6/D7/D8 | D9-1 |
| Production `VerifyPolicy` assembly (trusted-key store, channel pin, running version) | D7 | D9-1 |
| `spt send` WAN leg CLI wiring | D5 | D9-1 |
| Wake-edge (state→active) resurface hook | D8 | D9-2 |
| Dormancy resource budget + warm/cold policy (red-team #9) | ADR-0003 | D9-2/D9-3 |
| REQ-NOTIF / sync / update / msg two-host int legs | D5–D8 | D9-6 |
| gh-repo interim sync retirement | D6 | D9-6 |
| Relay rendezvous routing (D2f Q1) | D2f | **M5** (document, not build — no consumer until shells) |
| REQ-PAIR-7 subnet icon | D2 | **GUI milestone** (user 2026-06-04) |
| Adapter-payload propagation; `p-` tier per-file visibility filtering | D6/D7 | **post-v1** (unchanged) |

## Decisions (locked)

- **Two-host = env-gated tests + runbook.** Int tests gate on `SPT_TWO_HOST=1`
  (+ peer address/role env), skip silently otherwise — CI never runs them; the rig run
  is manual, scripted by `docs/TWO-HOST-RUNBOOK.md`, evidence tags live **on the gated
  tests** (real runs on real hosts). CI keeps the loopback int coverage it already has.
- **Trigger loops are thin calls over existing drivers** (`select_refs`/`request_sync`,
  `request_update`/`serve_update`, D8-2 notif emit/apply, `reconcile_after_sync`) —
  D9 wires *when*, the drivers already own *how*. Same posture as every D5–D8 deferral
  note. Per-loop period/enable knobs in `DaemonConfig`; loops named, observable, and
  stoppable like `run_pulse_loop`.
- **Pump loops journal with `None`** (gotcha #1) and snapshot stream tables before
  opening requester streams (gotcha #9) — inherited test discipline, now production
  discipline.
- **KH 7.4 activates when it binds:** transition echo communes (D9-2) are per-agent
  LLM-bound calls scheduled by the daemon — the moment the daemon hosts more than one
  agent's bounded call, `REQ-HAZARD-DAEMON-SCHED-NONBLOCKING` activates and the calls
  run off the shared scheduler.
- **Registry `Status` gains `Suspended`.** CONTEXT.md's registry status is
  active/dormant/suspended/offline; the wire enum is 3-state today. Additive variant,
  pre-release fleet — no compat machinery; the NDJSON decode posture (unknown-kind
  skip) is already the registry record's forward-compat story.
- **Resource blurb = `Option<String>` on the registry `Instance`** — rides the existing
  epoch lease + `RegistryUpdate` replication. No new record kind, no new store, no new
  merge surface (the lease orders blurb updates like any instance update). The
  "subnet resource registry" is a **projection**, exactly as CONTEXT.md defines it.
- **Fork = copy-then-diverge, never re-home** (ADR-0010): new identity, one-time
  BranchStore tier copy, join-time collision check, no ongoing sync, source untouched
  unless `--delete-source`.
- **Suspended is node-local mechanics, registry-advertised state**: dormant = harness
  session alive (warm); suspended = session closed, resume-on-wake (cold). Warm stays
  the default; auto-suspend ships **opt-in, default OFF**, global → node → endpoint
  override chain (CONTEXT.md policy, confirmed or amended by D9-3 numbers).

## Slices (each CI-green before the next)

1. **D9-1 — production trigger loops + VerifyPolicy assembly + `spt send` WAN CLI.**
   - `spt-daemon/src/peerloop.rs` (new): the per-subnet peer pump host. Holds/refreshes
     trusted-peer connections via `NetHost` (registry + trust-store sourced), then runs
     the pumps as scheduled thin calls:
     a. **sync** — context-write notify → peers pull; periodic anti-entropy
        (`select_refs` + `request_sync`); dormant→active freshness pull *seam* (the
        trigger arrives with D9-2's wake edge).
     b. **update** — periodic trusted-peer query via `request_update` under the
        **production `VerifyPolicy`**: trusted-key store, channel pin, running version
        (assembled here, the D7 deferral). Serve side already gated.
     c. **notif** — feed pump: local insert/dismiss → emit to subnet member conns;
        inbound apply (D8-2 drivers).
     d. **post-sync reconcile election** — `reconcile_after_sync` at pump completion
        (ADR-0013 wiring already exists; the pump calls it).
   - `DaemonConfig` knobs: `sync_period`, `update_check_period`, `notif_pump_period`
     (+ per-loop enable flags). Defaults conservative; document in MANIFEST/STORAGE
     as applicable.
   - `spt send` WAN leg: the CLI grows the off-node arm transparently (D5's deferral —
     resolution policy picks the instance; `id@node` pins).
   - Loopback E2E (`tests/peerloop.rs`): **the loops fire on their own** across two
     brokers — a context write on A appears on B with no manual driver call; an
     undelivered notif pumps over; a staged newer release self-heals B. Gotchas #1/#9.
   - Evidence: `[impl->REQ-INST-5]`/`[impl->REQ-UPD-1]`/`[impl->REQ-NOTIF-1]` on the
     respective pump arms (production wiring of already-activated reqs — tags on the
     real loop code, stages unchanged).

2. **D9-2 — resting-state model + transition echo + wake-edge resurface.**
   Activate **REQ-INST-3 → `[impl, unit]`**, **REQ-INST-4 → `[impl, unit]`**.
   - Explicit instance state machine (daemon-owned): **active → dormant** (driver
     detach, or attention shift — another instance of the id becomes
     most-recently-interacted via the existing `touch_active` recency);
     **dormant → suspended** (`spt suspend` / shell `api owner-shutdown`, or opt-in
     `auto-suspend-after` counted from dormancy onset; global → node → endpoint
     override chain, default OFF); **wake** = re-activation in place (state's there).
   - Registry advertisement follows transitions: `Status::Suspended` variant added;
     epoch-bumped updates on every transition (the lease orders them).
   - **Transition echo commune** on active → (dormant|suspended): `run_echo_commune`
     at the transition edge (KH 3.3 ordering — echo before any teardown), syncs to
     whichever instance activates next. `[impl->REQ-INST-4]`.
   - **Wake edge:** state→active transition fires `resurface_at_boundary(Wake)` (the
     D8 deferral — edge detection is real now that the state machine exists) and the
     dormant→active **freshness pull** (D9-1's seam consumes it).
   - KH 7.4: if the daemon now schedules >1 agent's bounded echo call, activate
     `REQ-HAZARD-DAEMON-SCHED-NONBLOCKING` and run the calls off the shared scheduler.
   - REQ-INST-6 (deferred-message instance-state gate) stays inactive — document the
     seam (the state machine is its precondition; the gate is M5).
   - Unit: transition table (detach, attention-shift, suspend, auto-suspend threshold,
     wake), echo-fires-once per edge, registry status follows, wake resurfaces.

3. **D9-3 — dormancy resource budget + warm/cold policy lock (red-team #9 gate).**
   Closes REQ-INST-3's open policy question with **measured numbers**.
   - Env-gated measurement harness (`SPT_BUDGET=1`, ignored in CI): spawn N dormant
     sessions on a real adapter on the rig, sample RSS / handle count / idle CPU per
     dormant seat and per suspended seat (the delta IS the warm cost).
   - Record in `docs/DORMANCY-BUDGET.md`: methodology, numbers per host
     (HFENDULEAM Windows + gravity Linux), the resulting policy.
   - Lock policy: warm default confirmed (or amended — the numbers decide),
     auto-suspend default OFF globally, node-overridable (constrained nodes may
     default ON), thresholds as config knobs. Amend ADR-0003 (#9 closed: "measured
     budget + warm/cold policy") + CONTEXT.md. `<!-- [doc->REQ-INST-3] -->`.

4. **D9-4 — REQ-INST-14 resource advertisement → `[doc, impl, unit]`.**
   - `resources: Option<String>` on registry `Instance` (additive, serde-default —
     absent = no blurb). Epoch lease orders updates; replication = existing
     `RegistryUpdate` path untouched.
   - Both-authored: config seeds the default blurb; `spt resources set <text>`
     (the agent refines its own at runtime) + `spt resources show [id]`.
   - Projection view: `spt resources list [--subnet S]` = `(id, node, blurb)` over
     **visible** rows only — reuses `resolve_visible`-path filtering, so exclusion
     leaks nothing by construction (ADR-0006 §6).
   - Doc tag in CONTEXT.md's existing *resource advertisement* entry; unit: blurb
     advertises/updates under the lease, hidden endpoint never appears in the
     projection, absent-blurb rows render clean.

5. **D9-5 — REQ-INST-15 immutable home subnet + `spt fork` → `[doc, impl, unit]`.**
   - **Home assignment at creation** (persisted on the endpoint record, identity dir):
     sole-subnet node → auto; multi-subnet node → must be specified (refuse-and-qualify,
     no silent guess); unpaired node → local-only until first join sets home.
     **No setter exists** — immutability by construction (ADR-0010).
   - Creation seeds `sync_subnets = [home]` (the `visibility.rs` documented seam, now
     real) + the adapter-at-creation rule (chosen from registered hostable adapters;
     changed only via launch/resume-under-new).
   - `spt fork <src> <new-id> --subnet <target> [--delete-source]`: new identity
     (fresh perch), join-time collision check in the target (default = source's bare
     name if free, else refuse with rename guidance), **one-time copy** of the live +
     project context tiers (BranchStore seed commit — copied-then-independent, no
     ongoing sync), fork's home = target. Source untouched unless `--delete-source`.
     Cross-node fork composes with instantiate-anywhere's consent gate — **same-node
     only in v1** (the remote arm is M5, with the gate).
   - ADR-0010 doc tags; unit: home auto/ask/local-only matrix, fork collision refusal,
     fork copies-then-diverges (a post-fork write on the source never reaches the fork),
     delete-source removes exactly the source.

6. **D9-6 — two-host E2E + runbook + gh-repo interim retirement.**
   - `SPT_TWO_HOST=1`-gated int tests (skip silently without the env; role/peer-addr
     via env): the M4-PLAN §D9b ladder — **pair → register → cross-node message →
     remote-drive → Psyche sync → update self-heal**, plus the notif cross-node leg
     (insert on A → fires on B; dismiss on B → replicates to A). Int evidence tags on
     these tests: `[int->REQ-NET-1]`, `[int->REQ-INST-5]`, `[int->REQ-UPD-1]`,
     `[int->REQ-NOTIF-1]` (+ pairing/registry reqs as the ladder touches them).
   - `docs/TWO-HOST-RUNBOOK.md`: the exact rig procedure (HFENDULEAM ↔ gravity-linux),
     env setup, run order, what green looks like; evidence (run output) captured in a
     runbook appendix when the rig run happens.
   - **Run the runbook on the rig.** Activate the touched reqs' `int` stages only
     after the real run is green.
   - **Retire the gh-repo interim**: the sister gh-repo-sync skill stands down after
     the proof (the D6 exit condition); `psyche-sync-setup` remains for the hub-mode
     seam but is no longer the sync path — note in CONTEXT.md.
   - Flake-ledger discipline: any rig flake gets a ledger entry, not a shrug.

7. **D9-7 — closeout sweep.**
   - Full `traceable-reqs.toml` audit: every M4 req either satisfied at its
     `required_stages` or explicitly deferred with a rationale comment (rule 5 —
     nothing silently pre-failed). `traceable-reqs check` green from repo root.
   - ROADMAP.md: M4 delivered (scope line met; deferred items annotated to M5/post-v1).
   - CONTEXT.md glossary amendments accumulated across D9 (resting states' final
     policy numbers, fork, resource advertisement, retired interim).
   - M4-PLAN + this plan get plan-status COMPLETE; CI matrix green
     (ubuntu + windows + traceability) verified FINAL via `gh run list` (gotcha #7).

## Out of scope (unchanged by D9)

- REQ-INST-6 deferred-message instance-state gate — M5 (seam documented in D9-2).
- REQ-PAIR-7 subnet icon — GUI milestone (user 2026-06-04).
- Cross-runner CI two-host job — M5 seam (runbook is the v1 proof).
- Remote fork arm (cross-node `spt fork`) — M5, lands with instantiate-anywhere's
  consent UX.
- REQ-REACH-2, REQ-EP-5, PresenceChannel full impl, cross-user generalizations —
  M5 / post-v1 per M4-PLAN §Out.
- Relay rendezvous routing (D2f Q1) — M5, no consumer until shells.
