File created successfully at: C:\Users\decid\Documents\projects\spt-core\MESH-D6-PLAN.md (file state is current in your context — no need to Read it back)

# Mesh-D6 — fan-out widen + hard cutover → MESH ON

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 6, **merged with the deferred D5b cutover** per the re-cut in `MESH-D5-PLAN.md` (2026-06-08). **This is the single commit that turns the mesh on.** The push target widens from the directly-paired set to **all roster members**, the `TrustStore` authorization path is deleted (hard cutover, no migration — decision 5), and the D0 staggered harness flips green. Activates **REQ-MESH-3** (impl, unit, int) and completes **REQ-MESH-5** (impl, unit).

## Why D5b and D6 are one commit (the coupling)

Deleting `TrustStore` and widening the fan-out are **inseparable**:

- `peerloop` derives its **push target** (who it dials + pushes rows to) from `trust.peers_in()` — the directly-paired set (`peerloop.rs:327,364`).
- Stripping pairing's trust write empties `peers.json`; the cutover deletes it. With no `peers.json`, `peerloop`'s narrow push returns nothing → directly-paired flows break.
- The only repair points `peerloop` at `roster.members_in()` — but the roster is **wide** (D4 made A learn C transitively). That swap **is** the D6 widen.

So there is no "delete `TrustStore` but keep the push narrow" intermediate. The cutover forces the widen; they ship together. D5b (shipped, `91083ef`) was the one genuinely separable, no-reach half: roster-anchoring the REQ-SUBNET-7 eviction.

## Pre-flight (carried, already CLEARED at D5)

The cutover creates a transient new↔old partition (seed-proof auth can't interop with pairwise). Acceptable because the D6 roll is a **coordinated manual fleet rebuild** (`git pull` + build + restart), out-of-band; ADR-0016 update *apply* is node-local and does **not** ride member connections. No self-update deadlock. (Re-verify nothing new since D5 rides member conns for the binary roll.)

## The widen (the load-bearing change — REQ-MESH-3)

`peerloop.rs` tick loop (~360): the push iterates `trust.peers_in(&sub.name)` → change to `roster.members_in(&sub.name)`. The roster is already loaded for the sync gate (`roster_path`, ~339) — reuse it; the push and the gate read one store. Drop `PumpPaths.trust` (`peerloop.rs:119,163`) and the `TrustStore::load_from(&paths.trust)` at ~327. `members_in` already excludes tombstoned entries (D3), so revoked/superseded keys are not dialed.

**Rows stay own-authored.** `advertise_local` already pushes only this node's own rows (no relay) — unchanged. The widen changes only *who we open a direct connection to*, never *whose rows we send*. KH 4.10 / 7.5 preserved verbatim (the milestone's whole point).

**Regression assert (REQ-MESH-3):** in the mesh int test, assert every applied registry row's `origin_node` (handshake-proven) equals the row's author — no third-party row is ever applied. This is the audit that the widen turned on *reach*, not *relay*.

## The cutover (delete the trust path — completes REQ-MESH-5)

1. **Strip trust from pairing** (`pairhost.rs`): drop `TrustStore::load` (78, 307), the `&mut trust` arg to `run_responder` (92) / `run_initiator` (336, 383), `trust.save()` (134, 407–409), and the `outcome.trust` log (135). The roster already records membership (D3: `record_roster_peer`, `refresh_self_roster`, `stamp_roster_responder`, the joiner's `merge`/adopt). Remove the `trust: &mut TrustStore` param from `run_responder`/`run_initiator` in `spt_net::net::pairing::wire` + everything they call on it (the pin write). Keep `outcome.trust` only if other fields need the struct; else drop the field.
2. **`propagate` serve gate** (`propagate.rs:105–112,133,172`): signature `trust: &TrustStore` → `roster: &RosterStore`; the two `trust.peers.iter().any(|p| p.pubkey_hex == origin_node)` checks → `roster.is_member`-in-any-subnet (iterate the node's subnets, or add a `roster.is_member_any(origin)` helper). Update the caller `dispatch.rs:326–327` (`TrustStore::load()` → `RosterStore::load()`).
3. **Demote warn-on-change** (REQ-MESH-5): the label-anchored `KeyChanged` path dies with `trust.rs`. At the REQ-SUBNET-7 re-pair detection point (`repair_evict_superseded`, which already runs on a same-(label,machine_id) different-key match), emit a **non-blocking awareness notif** — "machine M, last seen as K1, now presents K2" — never a gate. Absent machine_id ⇒ no notice (no false positive). Anchor on `intro.machine_id` (already in hand there).
4. **CLI repoint** (`spt/src/cli.rs`): the `subnet status --nodes` render (`subnet_status_rows`, ~2293/2306) + `subnet list` (~2770/2822) `trust.peers_in` → `roster.members_in`; the `1305` trust load → roster; `prune` (~2904–2961) → roster removal. Prune semantics: local **tombstone** (so the `is_member` gate drops it and a merge can't re-admit) — the *propagating, seed-rotating* revoke is D7; note that in the CLI help. Migrate the render unit tests (`cli.rs` ~6518–6693) trust→roster.
5. **Delete the store**: `crates/spt-store/src/trust.rs` (struct + 8 unit tests), `perch::trust_file` (the `peers.json` path) + its doc, the `lib.rs` `pub mod trust`. **Keep `perch::trust_dir()`** — it still homes `access.json` / `recent-outbound.json` / `grants.json` (those are not trust-store material; the dir name is incidental). `peers.json` has no writer/reader left ⇒ gone by deletion, no migration. Grep-verify zero `TrustStore` / `is_trusted` / `trust::` / `peers.json` references remain in non-comment code.
6. **Migrate tests**: `twohost.rs`, `propagate.rs`, and the `pairing/wire.rs` ceremony tests — replace `TrustStore` construction/assertions with `RosterStore` membership (seed via `merge_entry`/`upsert_self`, assert via `is_member`). `notifsync.rs`/`sync.rs`/`peerloop.rs`/`dispatch.rs` already migrated at D5a.
7. **The green flip**: un-ignore the D0 staggered harness (`mesh.rs`, `#[ignore]`); it asserts A↔C converge to see **and reach** each other (B offline at the critical step) + the all-online star (A reaches C, B never relays). It flips green here. This is REQ-MESH-3's int evidence.

## Tests

- **int (the acceptance, REQ-MESH-3):** the un-ignored `mesh.rs` staggered + star harness passes; the no-third-party-row regression assert holds (every applied row's origin == its author).
- **unit (REQ-MESH-5 completion):** `propagate` serve gate admits a roster member / refuses a non-member; the machine_id awareness notice fires on a same-machine re-key without blocking, absent machine_id ⇒ silent; CLI render reads the roster. Compile-enforced: `trust.rs` is gone, so any missed reader fails to build.
- **Regression:** D2 seed-proof int, D3 roster/wire, D4 propagation, D5b eviction, every migrated daemon test stay green; `--no-default-features` + workspace + clippy clean. Watch the **false-green trap**: read raw `cargo test`/`clippy` exit codes, never trust a piped `grep`.

## Done when

- `peerloop` push target is `roster.members_in`; the D0 harness is un-ignored and **green** (A↔C reach); the no-relay regression assert passes.
- `TrustStore` + `peers.json` + every call site are **removed, not left dead**; `trust_dir()` retained for access/grants; warn-on-change is a machine_id notice, not a gate.
- Pairing records membership in the roster only; D5b's REQ-SUBNET-7 eviction still acceptance-green.
- `traceable-reqs check` clean with **REQ-MESH-3 `[impl,unit,int]`** + **REQ-MESH-5 `[impl,unit]`** activated; REQ-SUBNET-7 still satisfied; KH 4.10 / 7.5 hazard tests still green.
- One commit: `feat(mesh)!: fan-out widen + delete TrustStore → mesh on (REQ-MESH-3, REQ-MESH-5) — Mesh-D6`.

## Explicitly NOT D6 (carry to D7+)

- **D7** — `spt subnet revoke` (propagating tombstone + timeboxed seed rotation), confidential seed push, re-seed grace, the per-conn `proven_subnets` epoch cross-check, cross-node un-tombstone.
- **D8** — `subnet status --nodes` probe concurrency.


Committing the D6 plan, then commune.


Bash

git add MESH-D6-PLAN.md && git commit -F - <<'EOF'
docs(mesh): MESH-D6-PLAN.md — fan-out widen + cutover → mesh on

JIT plan for the D6 phase (merged with the deferred D5b cutover per the
MESH-D5-PLAN re-cut): the single commit that turns the mesh on. Documents the
deletion↔widen coupling, the load-bearing peerloop push swap (trust.peers_in →
roster.members_in), the TrustStore deletion sweep (keep trust_dir for
access/grants), warn-on-change demotion to a machine_id notice, the D0 harness
green-flip, and the no-third-party-row regression assert. Activates REQ-MESH-3
+ completes REQ-MESH-5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -3

Commit D6 plan doc


warning: in the working copy of 'MESH-D6-PLAN.md', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 9b09b11] docs(mesh): MESH-D6-PLAN.md — fan-out widen + cutover → mesh on
 1 file changed, 54 insertions(+)
 create mode 100644 MESH-D6-PLAN.md
9b09b11 docs(mesh): MESH-D6-PLAN.md — fan-out widen + cutover → mesh on
91083ef fix(mesh): roster-anchor REQ-SUBNET-7 eviction — Mesh-D5b
e57818a feat(mesh): swap inbound gates is_trusted->roster is_member (REQ-MESH-5) — Mesh-D5a


D6 plan committed. Now commune.


Skill

Session arc: reviewed broader mesh plan + ADR-0017, found + fixed a latent D5a gap, shipped Mesh-D5b, re-cut the D5b/D6 boundary, wrote MESH-D6-PLAN.md. Resuming next session at D6.

IMMEDIATE NEXT = execute Mesh-D6 (the mesh-ON switch) per MESH-D6-PLAN.md. It is one commit, big + harder-to-reverse: deletes peers.json fleet-wide + turns the mesh on. Steps: (1) strip pairing-trust write (pairhost both legs + run_responder/run_initiator sigs in spt_net pairing/wire.rs); (2) propagate serve gate &TrustStore->&RosterStore + dispatch.rs:326 caller; (3) demote warn-on-change to a non-blocking machine_id notif at the repair_evict_superseded detection point; (4) THE WIDEN: peerloop push trust.peers_in -> roster.members_in (reuse the roster already loaded for the sync gate; drop PumpPaths.trust); (5) CLI repoint status/list/prune -> roster (prune = local tombstone; propagating revoke is D7); (6) delete trust.rs + perch::trust_file + lib.rs pub mod trust, KEEP perch::trust_dir() (still homes access/grants), grep-verify zero TrustStore/is_trusted/trust::/peers.json in non-comment code; (7) migrate twohost.rs/propagate.rs/pairing wire ceremony tests trust->roster; (8) un-ignore mesh.rs D0 staggered+star harness (flips GREEN — A<->C reach, B never relays) + add the no-third-party-row regression assert (every applied row origin == author); (9) activate REQ-MESH-3 [impl,unit,int] + complete REQ-MESH-5 [impl,unit]. Commit: feat(mesh)!: fan-out widen + delete TrustStore -> mesh on — Mesh-D6.

Key design call (why D5b/D6 re-cut): deletion and widen are INSEPARABLE. peerloop's narrow push reads trust.peers_in; stripping pairing-trust empties peers.json, forcing roster.members_in = the D6 widen. So "delete TrustStore at D5b, keep push narrow" (original plan) is unachievable. The one separable no-reach half was roster-anchoring the REQ-SUBNET-7 eviction — which D5a had silently BROKEN (gate moved to roster.is_member, but repair_evict_superseded still only dropped trust rows, so a re-keyed identity kept its roster entry + stayed authorized). D5b (91083ef) fixed it by tombstoning the superseded roster entry. Documented the re-cut in MESH-D5-PLAN.md.

Mesh arc: D0..D4 done, D5a done, D5b done (91083ef), D6 NEXT (mesh on), then D7 (revoke/rotate/re-seed + the deferred per-conn proven_subnets epoch cross-check) + D8 (status probe concurrency).

Process lesson reinforced: NEVER pipe cargo through grep and trust the exit (grep matching "error" exits 0 = false green). Read raw cargo exit codes. Used `; echo ===EXIT:$?===` this session — daemon suite + clippy + traceable-reqs all exit 0.

Broad status + end goal (unchanged): spt-core = harness-independent core for the agent ecosystem, clean-room rebuild of claude_skill_owl. M0-M8 shipped; v0.2.0 live on the reachable fleet. Post-M8 subnet full-mesh milestone in flight (fixes "subnet was the pairing graph, not a mesh" — non-directly-paired members invisible/unreachable; D6 is the fix landing). End goal = v1 with the rebuilt spt plugin (Claude Code downstream repo) working E2E = the acceptance proof. dev-freeform carries the unpushed mesh commit set (now through D5b + D6 plan doc) + earlier offline-name fix + daemon-start alias — USER CONTROLS THE PUSH. Queued untouched on disk: DAEMON-LIFECYCLE-PLAN.md.


Launching skill: spt:commune


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\plugins\cache\cplugs\spt\1.11.25\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

ARGUMENTS: Session arc: reviewed broader mesh plan + ADR-0017, found + fixed a latent D5a gap, shipped Mesh-D5b, re-cut the D5b/D6 boundary, wrote MESH-D6-PLAN.md. Resuming next session at D6.

IMMEDIATE NEXT = execute Mesh-D6 (the mesh-ON switch) per MESH-D6-PLAN.md. It is one commit, big + harder-to-reverse: deletes peers.json fleet-wide + turns the mesh on. Steps: (1) strip pairing-trust write (pairhost both legs + run_responder/run_initiator sigs in spt_net pairing/wire.rs); (2) propagate serve gate &TrustStore->&RosterStore + dispatch.rs:326 caller; (3) demote warn-on-change to a non-blocking machine_id notif at the repair_evict_superseded detection point; (4) THE WIDEN: peerloop push trust.peers_in -> roster.members_in (reuse the roster already loaded for the sync gate; drop PumpPaths.trust); (5) CLI repoint status/list/prune -> roster (prune = local tombstone; propagating revoke is D7); (6) delete trust.rs + perch::trust_file + lib.rs pub mod trust, KEEP perch::trust_dir() (still homes access/grants), grep-verify zero TrustStore/is_trusted/trust::/peers.json in non-comment code; (7) migrate twohost.rs/propagate.rs/pairing wire ceremony tests trust->roster; (8) un-ignore mesh.rs D0 staggered+star harness (flips GREEN — A<->C reach, B never relays) + add the no-third-party-row regression assert (every applied row origin == author); (9) activate REQ-MESH-3 [impl,unit,int] + complete REQ-MESH-5 [impl,unit]. Commit: feat(mesh)!: fan-out widen + delete TrustStore -> mesh on — Mesh-D6.

Key design call (why D5b/D6 re-cut): deletion and widen are INSEPARABLE. peerloop's narrow push reads trust.peers_in; stripping pairing-trust empties peers.json, forcing roster.members_in = the D6 widen. So "delete TrustStore at D5b, keep push narrow" (original plan) is unachievable. The one separable no-reach half was roster-anchoring the REQ-SUBNET-7 eviction — which D5a had silently BROKEN (gate moved to roster.is_member, but repair_evict_superseded still only dropped trust rows, so a re-keyed identity kept its roster entry + stayed authorized). D5b (91083ef) fixed it by tombstoning the superseded roster entry. Documented the re-cut in MESH-D5-PLAN.md.

Mesh arc: D0..D4 done, D5a done, D5b done (91083ef), D6 NEXT (mesh on), then D7 (revoke/rotate/re-seed + the deferred per-conn proven_subnets epoch cross-check) + D8 (status probe concurrency).

Process lesson reinforced: NEVER pipe cargo through grep and trust the exit (grep matching "error" exits 0 = false green). Read raw cargo exit codes. Used `; echo ===EXIT:$?===` this session — daemon suite + clippy + traceable-reqs all exit 0.

Broad status + end goal (unchanged): spt-core = harness-independent core for the agent ecosystem, clean-room rebuild of claude_skill_owl. M0-M8 shipped; v0.2.0 live on the reachable fleet. Post-M8 subnet full-mesh milestone in flight (fixes "subnet was the pairing graph, not a mesh" — non-directly-paired members invisible/unreachable; D6 is the fix landing). End goal = v1 with the rebuilt spt plugin (Claude Code downstream repo) working E2E = the acceptance proof. dev-freeform carries the unpushed mesh commit set (now through D5b + D6 plan doc) + earlier offline-name fix + daemon-start alias — USER CONTROLS THE PUSH. Queued untouched on disk: DAEMON-LIFECYCLE-PLAN.md.


write the D6 plan if it doesn't exist, then /commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


Download fresh Psyche context to diff against (it shifts during session).


Bash

$LIVE psyche-download doyle 2>&1 | tail -60; echo "===EXIT:$?==="

Download current Psyche context


<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="dev-freeform" head_sha="9b09b11a9709965f86e4c12247bcc9237d8f6575" head_subject="docs(mesh): MESH-D6-PLAN.md — fan-out widen + cutover → mesh on" commits_since="0" commits_unpulled="0"/>
**Session arc since last commune: D5 planned → D5a shipped.** Psyche was stale at "D5 IN PREP, Explore map pending, plan awaiting write." Delta below.

**Two design calls worth keeping (D5):**
1. **Gate on `is_member`, NOT plumb `proven_subnets` to the dispatcher.** The Explore map surfaced the real crux: the 5 gates run in the dispatcher, which has `origin_node` (hex) but NO conn handle — it can't read `conn_proven_subnets` without new broker-IPC plumbing. It doesn't need to: at D5 `is_member` (roster-listed ∧ ¬tombstoned) is **equivalent** to the proven set — a non-member can't reach a gate (D2 seed-proof drops it at connect), and there's no seed rotation yet (D7) to skew "in roster" from "proved-this-epoch." So `is_member`, loaded per-call (the existing Policy shape), zero plumbing. proven_subnets stays the connect-time enforcement; the per-conn crypto cross-check defers to D7. This kept D5a tiny.
2. **Split D5 into D5a (gate-read swap, parallel-run) + D5b (delete TrustStore).** The swap is no-behavior because pairing writes BOTH stores today, so gates reading roster ≡ reading trust. propagate's serve gate (the 6th `is_trusted` site) deferred to D5b — it's coupled to the TrustStore deletion (signature `&TrustStore`→`&RosterStore` + caller), so swapping it in D5a then re-touching in D5b is churn.

**Lesson that bit me (process):** `cargo build ... | grep -E "error"` returns **grep's** exit code, not cargo's — grep finding the error string exits 0, which I misread as a clean build. Twice produced a FALSE GREEN before I caught it. Always check `${PIPESTATUS[0]}` or read raw cargo output for build/test status, never trust the exit of a piped grep.

**The real D5a work was empirical test migration, not the swap.** Swapping the Policy field `trust`→`roster` (3 lib files) was 20 min. The integration harnesses (peerloop pump E2E, dispatch) load the roster from DISK and only seeded `trust.json` → ran red at runtime, not compile-time. Fixed by running each test and seeding `roster.json` wherever it seeded trust: peerloop needed it at BOTH B's canonical home AND A's explicit `a_root` (the pump derives roster path as `subnets.parent()/roster.json`); dispatch's shared `seed_gates` helper. The pattern: "wherever a test established trust for a GATE, establish roster membership."

**Reflection:** smooth once the model was right. The is_member-vs-proven_subnets call is the one I'd flag to a reviewer — it looks like I skipped the cryptographic gate, but the crypto IS enforced (at connect, D2); the roster gate is the directory/revocation half. Documented inline + in the plan so it doesn't read as a hole.<project-context-resolved name="spt-core"/>
**Mesh-D5a SHIPPED — commit `e57818a` on `dev-freeform` (unpushed). D5 is HALF done.**

`MESH-D5-PLAN.md` written (two-commit decomposition). D5a landed: the 7 dispatcher inbound gates (registry×3, sync×2, notif×2) now authorize on `RosterStore::is_member(subnet, origin)` instead of `TrustStore::is_trusted`. Loaded per-call via the existing `*Policy::load()` shape (`RegistryGatePolicy`/`SyncPolicy`/`NotifPolicy` field `trust: TrustStore` → `roster: RosterStore`). Parallel-run: pairing still writes `peers.json`, so it's a verified no-behavior swap. Push target stays NARROW (D6 widens). Gate unit tests + 4 integration harnesses (notifsync/sync/peerloop/dispatch) migrated to seed the roster. **Green:** full daemon suite 0 failures, clippy + workspace + no-default clean, traceable-reqs 172/0 (REQ-MESH-5 still inactive `[]` — tags are forward evidence; activates at D5b when impl+unit fully met).

**IMMEDIATE NEXT = Mesh-D5b** (the hard cutover — large, harder-to-reverse deletion sweep; checkpointed before it deliberately). Steps, all in `MESH-D5-PLAN.md §D5b`:
1. Strip trust from pairing — `pairhost.rs` both legs (responder ~78–140, joiner ~307) + `run_responder`/`run_initiator` sigs in `pairing/wire.rs`. Roster already carries membership (D3).
2. `propagate.rs` serve gate (the 6th `is_trusted`, `trust.peers.iter()...`) → roster `is_member`-in-any-subnet; signature `trust: &TrustStore` → `roster: &RosterStore` + the `dispatch.rs:326` caller.
3. `registryhost::repair_evict_superseded` (REQ-SUBNET-7, `&mut TrustStore` + `trust.revoke`) → evict the superseded ROSTER entry + registry rows. Keep the re-pair acceptance green; tests at registryhost.rs ~912/949 migrate.
4. warn-on-change → machine_id awareness notice: delete the label-anchored `KeyChanged` (dies with trust.rs); at the repair-evict detection point emit a NON-blocking notif ("machine M, last seen K1, now K2"); absent machine_id ⇒ no notice.
5. CLI repoint (`spt/src/cli.rs`): `subnet status --nodes`/`list` `peers_in`→`roster.members_in` (1305/2306/2822); `prune` (2904–2961) → roster LOCAL removal (`members.retain`; propagating revoke+tombstone is D7).
6. DELETE: `crates/spt-store/src/trust.rs` (+ its 8 unit tests), `perch::trust_file`/`trust_dir`, `lib.rs pub mod trust`, `peers.json` (no writers/readers left — no migration code, decision 5). Grep-verify zero `TrustStore`/`is_trusted`/`peers.json` remain.
7. Migrate `twohost.rs`/`propagate.rs`/`pairing wire.rs` ceremony tests trust→roster.
8. Activate REQ-MESH-5 `[impl,unit]`; keep REQ-SUBNET-7 green.

Pre-D5 deadlock check already CLEARED (manual fleet rebuild, out-of-band; ADR-0016 apply is node-local). peerloop test fix proved the roster path derivation: `paths.subnets.parent().join("roster.json")` = canonical `identity/roster.json`.

**Mesh arc:** D0✅ D1✅ D2✅ D3✅ D4✅ **D5a✅ ← just shipped · D5b ← NEXT** (cutover) · D6 fan-out widen → MESH ON (D0 staggered harness flips green; `mesh.rs` is `#[ignore]`d until then) · D7 revoke/rotate/re-seed + the deferred per-conn proven_subnets epoch cross-check · D8 status-probe concurrency.

**Broad status + end goal (unchanged):** spt-core = harness-independent core for the agent ecosystem, clean-room rebuild of `claude_skill_owl`. M0–M8 shipped; v0.2.0 live on the reachable fleet. Post-M8 **subnet full-mesh** milestone in flight (fixes "subnet was the pairing graph, not a mesh" — non-directly-paired members invisible/unreachable). **End goal = v1 with the rebuilt `spt` plugin (Claude Code downstream repo) working E2E = the acceptance proof.** `dev-freeform` carries the unpushed mesh commit set (now through D5a) + earlier offline-name fix + daemon-start alias — **user controls the push.** Queued untouched on disk: `DAEMON-LIFECYCLE-PLAN.md`.===EXIT:0===


Psyche stale at "D5a shipped, D5b NEXT (original cutover sketch)" — no pending commune. Delta: D5b actually shipped but **re-cut** (it's now the eviction fix, not the cutover); cutover+widen folded into D6; latent D5a gap found+fixed; D6 plan written; next is D6 not D5b. In-project → both slices.


Write

<live-context>
**Delta since last commune (you're stale at "D5a shipped, D5b=cutover NEXT"): D5b SHIPPED but RE-CUT, and the original D5b plan was unachievable.**

**The design call that changed the phase boundary.** Reviewing the original D5b ("delete TrustStore, keep push narrow") against the docs (user asked me to use judgment on SUBNET-MESH-PLAN + ADR-0017), I found it's **internally impossible**: peerloop's narrow push reads `trust.peers_in`. Stripping pairing's trust write empties `peers.json`; with no narrow source, the only repair points peerloop at `roster.members_in` — but the roster is WIDE (D4 made A learn C transitively), so that swap IS the D6 fan-out widen. **Deletion and widen are inseparable** — there is no "delete TrustStore but stay narrow" intermediate. So the cutover+widen fold into D6 (one mesh-on commit), and D5b shrank to the one genuinely separable, no-reach half.

**That separable half was a latent bug D5a silently shipped.** D5a moved every inbound gate to `roster.is_member`, but `repair_evict_superseded` still only dropped the (no-longer-consulted) trust row. So a re-keyed identity kept its roster entry and **stayed a member** — the REQ-SUBNET-7 re-pair overwrite was ineffective against the new gate. The eviction test only asserted `trust.is_trusted` cleared, never the roster, so it didn't catch it. D5b (`91083ef`) fixes it: tombstone the superseded roster entry (D3 grow-set primitive) alongside the trust drop. Test now asserts `!is_member` in the ceremony subnet + still-member elsewhere.

**Reflection:** the re-cut is the call I'd flag to a reviewer — I deviated from the written plan, but the plan was wrong (the coupling makes its D5b literally uncompilable), and the deviation surfaced a real correctness gap. Both the plan doc (MESH-D5-PLAN re-cut note) and the new MESH-D6-PLAN spell out the coupling so it doesn't read as scope drift. The grep-false-green lesson you already hold paid off — used `; echo ===EXIT:$?===` throughout; daemon suite + clippy + traceable-reqs all genuinely exit 0.
</live-context>
<project-context>
**Mesh-D5b SHIPPED — `91083ef` on `dev-freeform` (unpushed). D6 plan written (`9b09b11`).**

D5b ≠ the cutover you have queued. Re-cut (documented in `MESH-D5-PLAN.md`): D5b = roster-anchor the REQ-SUBNET-7 eviction only (the latent-gap fix above). `repair_evict_superseded` now takes `&mut RosterStore` too and tombstones the superseded entry; caller `pairhost.rs` passes `&mut roster`. TrustStore/pairing-write/propagate/peerloop-narrow/CLI all still on trust (≡ roster at D5). Green: 14 registryhost + full daemon suite + clippy + traceable-reqs all exit 0. REQ-MESH-5 still inactive `[]` (tags forward evidence; activates at D6).

**IMMEDIATE NEXT = Mesh-D6** — NOT the old "D5b". Full JIT plan in `MESH-D6-PLAN.md`. The single mesh-ON commit (big, harder-to-reverse: deletes `peers.json` fleet-wide, no migration). Steps: (1) strip pairing-trust write (pairhost both legs + `run_responder`/`run_initiator` sigs in `spt_net pairing/wire.rs`); (2) propagate serve gate `&TrustStore`→`&RosterStore` + `dispatch.rs:326` caller; (3) demote warn-on-change → non-blocking machine_id notif at the `repair_evict_superseded` detection point; (4) **THE WIDEN** — peerloop push `trust.peers_in`→`roster.members_in` (reuse the roster already loaded for the sync gate; drop `PumpPaths.trust`); (5) CLI repoint status/list/prune→roster (prune = local tombstone; propagating revoke is D7); (6) delete `trust.rs` + `perch::trust_file` + `lib.rs pub mod trust`, **KEEP `perch::trust_dir()`** (still homes access/grants — the plan I wrote corrects the psyche's "delete trust_dir"), grep-verify zero `TrustStore`/`is_trusted`/`trust::`/`peers.json` in non-comment code; (7) migrate twohost/propagate/wire ceremony tests trust→roster (notifsync/sync/peerloop/dispatch already done at D5a); (8) un-ignore `mesh.rs` D0 staggered+star harness (flips GREEN — A↔C reach, B never relays) + add the no-third-party-row regression assert (every applied row origin == author); (9) activate REQ-MESH-3 `[impl,unit,int]` + complete REQ-MESH-5 `[impl,unit]`. Commit: `feat(mesh)!: fan-out widen + delete TrustStore → mesh on — Mesh-D6`.

**Mesh arc:** D0–D4 ✅ · D5a ✅ · **D5b ✅ (`91083ef`)** · **D6 ← NEXT (mesh on)** · D7 revoke/rotate/re-seed + deferred per-conn proven_subnets epoch cross-check · D8 status-probe concurrency.

`dev-freeform` now carries the mesh set through D5b + the D6 plan doc (+ earlier offline-name fix + daemon-start alias) — **user controls the push.** Queued untouched on disk: `DAEMON-LIFECYCLE-PLAN.md`.
</project-context>


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\doyle-commune.md (file state is current in your context — no need to Read it back)

<live-context>
**Delta since last commune (you're stale at "D5a shipped, D5b=cutover NEXT"): D5b SHIPPED but RE-CUT, and the original D5b plan was unachievable.**

**The design call that changed the phase boundary.** Reviewing the original D5b ("delete TrustStore, keep push narrow") against the docs (user asked me to use judgment on SUBNET-MESH-PLAN + ADR-0017), I found it's **internally impossible**: peerloop's narrow push reads `trust.peers_in`. Stripping pairing's trust write empties `peers.json`; with no narrow source, the only repair points peerloop at `roster.members_in` — but the roster is WIDE (D4 made A learn C transitively), so that swap IS the D6 fan-out widen. **Deletion and widen are inseparable** — there is no "delete TrustStore but stay narrow" intermediate. So the cutover+widen fold into D6 (one mesh-on commit), and D5b shrank to the one genuinely separable, no-reach half.

**That separable half was a latent bug D5a silently shipped.** D5a moved every inbound gate to `roster.is_member`, but `repair_evict_superseded` still only dropped the (no-longer-consulted) trust row. So a re-keyed identity kept its roster entry and **stayed a member** — the REQ-SUBNET-7 re-pair overwrite was ineffective against the new gate. The eviction test only asserted `trust.is_trusted` cleared, never the roster, so it didn't catch it. D5b (`91083ef`) fixes it: tombstone the superseded roster entry (D3 grow-set primitive) alongside the trust drop. Test now asserts `!is_member` in the ceremony subnet + still-member elsewhere.

**Reflection:** the re-cut is the call I'd flag to a reviewer — I deviated from the written plan, but the plan was wrong (the coupling makes its D5b literally uncompilable), and the deviation surfaced a real correctness gap. Both the plan doc (MESH-D5-PLAN re-cut note) and the new MESH-D6-PLAN spell out the coupling so it doesn't read as scope drift. The grep-false-green lesson you already hold paid off — used `; echo ===EXIT:$?===` throughout; daemon suite + clippy + traceable-reqs all genuinely exit 0.
</live-context>
<project-context>
**Mesh-D5b SHIPPED — `91083ef` on `dev-freeform` (unpushed). D6 plan written (`9b09b11`).**

D5b ≠ the cutover you have queued. Re-cut (documented in `MESH-D5-PLAN.md`): D5b = roster-anchor the REQ-SUBNET-7 eviction only (the latent-gap fix above). `repair_evict_superseded` now takes `&mut RosterStore` too and tombstones the superseded entry; caller `pairhost.rs` passes `&mut roster`. TrustStore/pairing-write/propagate/peerloop-narrow/CLI all still on trust (≡ roster at D5). Green: 14 registryhost + full daemon suite + clippy + traceable-reqs all exit 0. REQ-MESH-5 still inactive `[]` (tags forward evidence; activates at D6).

**IMMEDIATE NEXT = Mesh-D6** — NOT the old "D5b". Full JIT plan in `MESH-D6-PLAN.md`. The single mesh-ON commit (big, harder-to-reverse: deletes `peers.json` fleet-wide, no migration). Steps: (1) strip pairing-trust write (pairhost both legs + `run_responder`/`run_initiator` sigs in `spt_net pairing/wire.rs`); (2) propagate serve gate `&TrustStore`→`&RosterStore` + `dispatch.rs:326` caller; (3) demote warn-on-change → non-blocking machine_id notif at the `repair_evict_superseded` detection point; (4) **THE WIDEN** — peerloop push `trust.peers_in`→`roster.members_in` (reuse the roster already loaded for the sync gate; drop `PumpPaths.trust`); (5) CLI repoint status/list/prune→roster (prune = local tombstone; propagating revoke is D7); (6) delete `trust.rs` + `perch::trust_file` + `lib.rs pub mod trust`, **KEEP `perch::trust_dir()`** (still homes access/grants — the plan I wrote corrects the psyche's "delete trust_dir"), grep-verify zero `TrustStore`/`is_trusted`/`trust::`/`peers.json` in non-comment code; (7) migrate twohost/propagate/wire ceremony tests trust→roster (notifsync/sync/peerloop/dispatch already done at D5a); (8) un-ignore `mesh.rs` D0 staggered+star harness (flips GREEN — A↔C reach, B never relays) + add the no-third-party-row regression assert (every applied row origin == author); (9) activate REQ-MESH-3 `[impl,unit,int]` + complete REQ-MESH-5 `[impl,unit]`. Commit: `feat(mesh)!: fan-out widen + delete TrustStore → mesh on — Mesh-D6`.

**Mesh arc:** D0–D4 ✅ · D5a ✅ · **D5b ✅ (`91083ef`)** · **D6 ← NEXT (mesh on)** · D7 revoke/rotate/re-seed + deferred per-conn proven_subnets epoch cross-check · D8 status-probe concurrency.

`dev-freeform` now carries the mesh set through D5b + the D6 plan doc (+ earlier offline-name fix + daemon-start alias) — **user controls the push.** Queued untouched on disk: `DAEMON-LIFECYCLE-PLAN.md`.
</project-context>


Commune written to `.claude/doyle-commune.md` — Self listener consumes on next poll. Wrap-up:

**Shipped this session (`dev-freeform`, unpushed):**
- `91083ef` — Mesh-D5b: roster-anchor REQ-SUBNET-7 eviction (fixes latent D5a gate gap)
- `9b09b11` — MESH-D6-PLAN.md

**Decision:** D5b/D6 re-cut. Deletion ⟺ widen inseparable, so cutover folds into D6. D5b = the separable no-reach correctness fix. Recorded in both plan docs.

**Next session = Mesh-D6** (the mesh-ON switch) per `MESH-D6-PLAN.md`. Big, harder-to-reverse: deletes `peers.json` fleet-wide, turns mesh on, D0 harness flips green.

Psyche synced. User controls the push. Resume at D6.