# M4-D6 — cross-node Psyche sync (P2P), retire gh-repo interim (JIT plan)

**Status:** D6a COMPLETE (D6a-1 `8b42452`, D6a-2 `1ef9802` — CI-green); D6b COMPLETE
(D6b-1 `7e9c2b5` CI-green: marker v2 + vectors + decide_merge + conflict artifacts +
hazard 6.6; D6b-2 `50620e4`: bounded turn driver (D7.5a) + reconcile_file +
is_reconciler election). D6c COMPLETE (D6c-1 `f08aedc`: BranchStore bundle plumbing
+ `syncmerge::apply_fetched` per-file vector driver, REQ-NET-3 activated; D6c-2:
SyncRecord wire + SyncPolicy server gate + serve_sync/request_sync/select_refs +
reconcile_after_sync election wiring + loopback two-daemon E2E + FAULT-MATRIX rows
14–17, REQ-INST-5 activated). **Production trigger loops** (context-write notify →
peers pull; dormant→active freshness pull; periodic anti-entropy) are thin calls
over `select_refs` + `request_sync` once the daemon lifecycle holds peer conns —
wired at D9 with the two-host topology (the same deferral as the D5 serve loops'
daemon dispatch). Remaining D6 exit: D9 two-host int legs; gh-repo interim sync
retires after the D9 proof.
Drafted 2026-06-03 at D6 start; all design forks resolved with user → **ADR-0013**;
decisions mirrored into M4-PLAN §D6b/§D7.5a, CONTEXT.md, STORAGE.md, KNOWN-HAZARDS
6.5/6.6.

## Goal

The mind follows the user across machines: an endpoint's **live context** replicates to
all its instances, **project context** only to same-project instances (loopback
two-daemon in CI; two-host at D9). A concurrent cross-node write is **surfaced and
Psyche-reconciled, never silently dropped**. Reqs: REQ-NET-3, REQ-INST-2, REQ-INST-5,
REQ-STORE-1, REQ-HAZARD-DIRECT-WRITE-PRECEDENCE (distributed generalization).

## Decisions (locked — ADR-0013, user-confirmed 2026-06-03)

- **Merge order = version vectors, wall-clock never orders.** Marker grows
  `spt:node=<node_id> spt:vector=<node:epoch,…>`; entries from each node's monotonic
  `EpochSource` (`spt-store/src/epoch.rs` — the same counter behind the D3b lease, the
  planned unification). Dominate→accept, dominated→drop, concurrent→surface. A
  missing/legacy vector = ⊥ (dominated by any vector; two ⊥s fall back to the local
  6.5 window rule).
- **Concurrent → Psyche auto-reconcile, single reconciler.** Both versions persist as
  **tracked conflict artifacts** (deterministic content-hash names — idempotent,
  replicate like any context file; local working file untouched). The **active
  instance's node** runs one bounded, stdin-fed, **stdout-captured** Psyche turn over
  both versions; fallback reconciler = lowest node id among holders. Merged write =
  `join(vA,vB)` + reconciler bump — dominates both parents, clears artifacts
  subnet-wide. Turn bounded (5.3) + output validated (non-empty, marker-parseable);
  any failure leaves artifacts for retry at next sync/activation + fires the event
  seam (D8 notif producer later). Reconciled output is `source=llm` — the 6.5
  direct-write window still applies.
- **D7.5a pulled forward into D6b.** The bounded stdout-captured Psyche turn driver
  (NOT the `Stdio::null()` interim, 7.3) is built here; D7.5 keeps b/c (taxonomy +
  relay/sanitize). Driver code tagged `[impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]` as
  evidence, but that req **activates at D7.5** (full invariant needs the relay).
- **Transport = git-native bundles over broker QUIC streams; the DAG is shared.**
  Pull-based, **ref-scoped**: a node requests `a-<id>` for endpoints with
  `synced(E,S)` (the D3e gate, finally consumed) and `p-<project>` only for projects
  it hosts — two-tier scoping falls out of the pull model, no new registry fields.
  Responder builds an incremental `git bundle` (`^have` exclusions); requester fetches
  from it + merges with the vector driver; first contact merges unrelated seeds once
  (`--allow-unrelated-histories`). Divergent per-node histories rejected.
- **BranchStore v1 = git-CLI-backed** (system `git`; the `git pull` handoff seam
  already assumes it) behind a trait so gitoxide is a later drop-in. Commit-per-write
  latency measured in D6a tests (the ADR-0011 spike, empirically).
- **Hub mode (gh opt-in) stays a deferred seam** — D6 is P2P only. Interim
  gh-repo-sync skill retires after the D9 two-host proof.

## Pieces (build order)

1. **D6a — context store realization** (REQ-STORE-1, REQ-INST-2).
   `spt-store::branchstore`: ensure-repo/ensure-branch, **commit = checkpoint, tip =
   resume**, linked worktree views with relative paths (`worktree.useRelativePaths`,
   Git ≥ 2.48; else `git worktree repair` fallback — STORAGE §Worktree portability).
   `tracked/` layout + path fns in `perch.rs` (`agents/<id>/live-context.md` on
   `a-<id>`; `projects/<pid>/<id>/project-context.md` on `p-<pid>` —
   REQ-HAZARD-SINGLE-PATH-SOURCE tags). `project_id` derivation (git remote URL →
   folder-name fallback, STORAGE §project_id). Ingest migration: commune drops'
   `<live-context>`/`<project-context>` slices route into the tier files
   (precedence-guarded writes, `ingest.rs` folds there instead of flat `context.md`);
   resume/preload reads follow. `spt rename` context-branch arm (the REQ-INST-11
   seam noted at D3f: `a-<old>` → `a-<new>`).
2. **D6b — distributed precedence + Psyche reconcile** (extends
   REQ-HAZARD-DIRECT-WRITE-PRECEDENCE; new hazard req below).
   `VersionVector` type + dominance (pure, hermetic); marker v2 parse/render
   (back-compat: v1 markers = ⊥ vector); per-file vector merge fn; conflict artifact
   write/clear; reconciler election (registry Active row → that node; none → lowest
   node id); **the bounded stdout-captured Psyche turn driver** (per-turn `--resume`,
   timeout 5.3, captured stdout = the merged file candidate); reconcile flow + output
   validation + retry-on-failure; event seam record. Negative tests: turn timeout,
   garbage output, harness absent — artifacts persist every time.
3. **D6c — bundle sync over the wire + gates** (REQ-NET-3, REQ-INST-5; consumes
   REQ-INST-13). NDJSON control records (`SyncRequest { refs, have_tips }` /
   bundle-chunk records riding the D5c xfer chunking) over broker streams; responder
   ref-scope enforcement (**`synced(E,S)` + visibility gate checked server-side too**
   — a peer must not be able to pull a hidden/unsynced mind, fail-closed); requester
   fetch + driver merge + two-parent merge commit; sync triggers: context-write
   notify → peers pull, **activation freshness feed** (dormant→active pulls first —
   the cross-instance context-freshness auto-feed + the D5c handoff seam's
   fresh-with-preload "mind" leg), periodic anti-entropy backstop. Loopback
   two-daemon tests: two-tier scoping (live everywhere, project same-project only),
   unsynced/hidden refused server-side, concurrent-write conflict → artifacts on both
   nodes → reconcile on active node → merged write converges everywhere.
4. **FAULT-MATRIX rows** (same change as the piece that lands them): stream loss
   mid-bundle (re-pull idempotent), partition concurrent writes (conflict path),
   reconcile turn failure (artifacts persist), registry ambiguity during election
   (re-conflict of merged outputs detected, not lost).

## Requirement activation (rule 5)

- **REQ-STORE-1, REQ-INST-2** → `[impl, unit]` at D6a.
- **New (rule 3): `REQ-HAZARD-CONFLICT-BOTH-PRESERVED`** — a surfaced concurrent pair
  is durably preserved (both versions) until a dominating write clears it; no
  reconcile path may discard an unmerged version. Register in `traceable-reqs.toml`
  at D6b start; `[impl, unit]` at D6b.
- **REQ-HAZARD-DIRECT-WRITE-PRECEDENCE** — stays `[impl, unit]`; D6b adds the
  multi-writer/node-id evidence.
- **REQ-NET-3, REQ-INST-5** → `[impl, unit]` at D6c. `int` = D9 two-host E2E.
- **REQ-HAZARD-PSYCHE-OUTBOUND-PROXY** — evidence tagged at D6b, **activates at D7.5**.

## NOT in D6

- Hub mode (gh opt-in) — deferred seam; interim skill retires at D9.
- D7.5b/c (event taxonomy + daemon relay/sanitize), D8 notif (D6 emits event seam only).
- memformat tier-tagged topics / block-level OCC (CONTEXT-MEMORY §Deferred) — the
  two-tier *files* land here; the schema evolution is post-v1.
- Two-host integration — D9 (loopback shapes only here).

## Conventions (carried)

- NO `cargo fmt`. Tag `[impl->REQ-…]` / `[unit->REQ-…]` on real evidence.
  `traceable-reqs check` from repo root before done. Linux CI clippy `-D warnings` is
  the real gate. Push each slice → watch green before next (verify FINAL via
  `gh run list --json conclusion`).
  Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
