# Phase 35.2: psyche-sync-setup-data-loss-reconcile-bootstrap-determinism — Pattern Map

**Mapped:** 2026-05-28
**Files analyzed:** 6 in-tree + 3 new tests
**Analogs found:** 9 / 9 (every new symbol has a precedent in-tree)

## File Classification

| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `src/common/tracked.rs` (`ensure_seed` cold path edit) | service / bootstrap | batch (single deterministic commit-tree) | self — existing `commit-tree` invocation at `tracked.rs:238-254` | exact (in-place edit) |
| `src/common/sync.rs` — new `reconcile_against_remote` helper | service | request-response (git shellout fan-out per branch) | `sync.rs::pull_branch` (`281-307`) — fetch+rebase pattern | exact (same rebase policy, different scope) |
| `src/common/sync.rs` — new `push_per_ref` helper | service | batch (per-branch fan-out with outcome capture) | `sync.rs::push_branch` (`311-319`) + `enumerate_existing_worktrees` (`424-440`) — per-element loop | exact |
| `src/common/sync.rs` — new `PerRefOutcome` / `ReconcileVerdict` types | model | data-shape | `sync.rs::SyncError` enum (`390-413`) — module-local enum with `Display` | role-match |
| `src/common/sync.rs` — `accept_flow` signature change to `Result<(String, Vec<PerRefOutcome>), SyncError>` | controller (setup orchestrator) | request-response | self — existing `accept_flow` at `sync.rs:482-600` | exact (in-place edit) |
| `src/common/sync.rs` — `accept_flow` insert: seed origin wire + fetch + reconcile + per-ref push | controller | request-response | existing step 4 worktree loop (`sync.rs:534-559`) + step 5 push (`561-568`) | exact |
| `src/owl/psyche_sync_setup.rs::run` — dispatcher per-ref status-tag emit (stderr) | controller / dispatch | request-response | existing `match accept_flow` arms at `psyche_sync_setup.rs:61-73` | exact (extend in place) |
| `tests/sync_two_machine_attach.rs` (NEW, cross-platform) | test | batch (helper-direct) | `tests/sync_pull_push.rs` (full file) | exact shape |
| `tests/sync_reconcile_against_remote.rs` (NEW, cross-platform) | test | request-response | `tests/sync_pull_push.rs` | role-match |
| `tests/sync_accept_flow_two_machine.rs` (NEW, `#[cfg(unix)]`) | test | request-response | `sync.rs` in-module `fake_gh` module (`1226-1401`) | exact shape |
| `CLAUDE.md` — status tag inventory append | config / doc | n/a | line 56 existing inventory | exact |

---

## Pattern Assignments

### `src/common/tracked.rs::ensure_seed` (service, batch — cold-path bootstrap)

**Analog:** Self, `src/common/tracked.rs:228-254` (the existing `hash-object` + `commit-tree` block). Edit replaces ONLY the `commit-tree` invocation (lines 238-254) with an inline `Command` that sets env vars; lines 228-235 (empty-tree hash) and lines 258-271 (`update-ref` + `symbolic-ref`) stay verbatim.

**Existing constants to reuse** (`tracked.rs:52-53, 59`):
```rust
const BOOTSTRAP_NAME: &str = "spt-bootstrap";
const BOOTSTRAP_EMAIL: &str = "spt@local";
const SEED_TIMEOUT_MS: u64 = 2000;
```

**Existing commit-tree shape (lines 238-254) — pattern to mutate:**
```rust
// 6. Build the bootstrap commit (Pitfall 3: synthetic identity via -c).
let commit_sha = git::run_git_checked(
    &[
        "-C", &seed_s,
        "-c", &format!("user.name={}", BOOTSTRAP_NAME),
        "-c", &format!("user.email={}", BOOTSTRAP_EMAIL),
        "commit-tree",
        empty_tree,
        "-m",
        "init: tracked seed",
    ],
    None,
    timeout,
)
.map_err(map_git_err)?;
let commit_sha = commit_sha.trim();
```

**Replacement pattern (RESEARCH Option B inline, hide_window + env + .output()):**
```rust
// 6. Build the bootstrap commit — DETERMINISTIC: epoch-locked dates so every
//    machine's main converges on the byte-identical SHA
//    (b3ea6a321a6c9a0ee90ae821820bd9777f8ff8b6). Inline Command because
//    run_git_checked has no envs param; this is the ONLY env-var commit-tree
//    call site (Phase 35.2 Fix 2).
let commit_sha = {
    let mut cmd = std::process::Command::new("git");
    crate::common::process::hide_window(&mut cmd);
    let out = cmd
        .args(&[
            "-C", &seed_s,
            "-c", &format!("user.name={}", BOOTSTRAP_NAME),
            "-c", &format!("user.email={}", BOOTSTRAP_EMAIL),
            "commit-tree",
            empty_tree,
            "-m", "init: tracked seed",
        ])
        .env("GIT_COMMITTER_DATE", "1970-01-01T00:00:00Z")
        .env("GIT_AUTHOR_DATE",    "1970-01-01T00:00:00Z")
        .output()
        .map_err(|e| TrackedError::Io(e))?;
    if !out.status.success() {
        let stderr = String::from_utf8_lossy(&out.stderr).to_string();
        return Err(TrackedError::GitFailed(stderr));
    }
    String::from_utf8_lossy(&out.stdout).trim().to_string()
};
```

**Error handling pattern** (mirrors `map_git_err` semantics from `tracked.rs:124-137`):
```rust
// On nonzero exit, surface stderr directly through TrackedError::GitFailed —
// same Display path the existing map_git_err uses for GitError::Nonzero.
```

**`hide_window` is mandatory** (every existing Command::new("git") / Command::new("gh") in this codebase calls it — `sync.rs:174, 488, 519`, `psyche_sync_setup.rs:93, 107`). Windows-portability invariant.

---

### `src/common/sync.rs` — new `reconcile_against_remote` (service, batch fan-out)

**Analog:** `src/common/sync.rs::pull_branch` (`281-307`) — same rebase policy.

**Imports pattern** (already present at top of `sync.rs:48-53` — no new imports needed):
```rust
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::common::git::{self, GitError};
use crate::common::owlery::{self, SyncSettings, SyncState};
use crate::common::time::now_iso_utc;
```

**Existing rebase policy to mirror** (`sync.rs:281-307`):
```rust
pub fn pull_branch(branch: &str, worktree: &Path) -> Result<(), GitError> {
    let _ = abort_stale_rebase(worktree);
    let _ = git::run_git_checked(
        &["-C", &worktree.to_string_lossy(), "stash", "--include-untracked"],
        None, SYNC_TIMEOUT,
    );
    git::run_git_checked(
        &["-C", &worktree.to_string_lossy(), "fetch", "origin", branch],
        None, SYNC_TIMEOUT,
    )?;
    let r = git::run_git_checked(
        &["-C", &worktree.to_string_lossy(),
          "rebase", "-X", "theirs", &format!("origin/{}", branch)],
        None, SYNC_TIMEOUT,
    );
    classify_and_record_outcome(&r, branch);
    r.map(|_| ())
}
```

**Existing helper to reuse** (`sync.rs:449-465`):
```rust
fn list_local_branches(seed: &Path) -> Result<Vec<String>, GitError> {
    let out = git::run_git_checked(
        &["-C", &seed.to_string_lossy(),
          "branch", "--format=%(refname:short)"],
        None, REBASE_ABORT_TIMEOUT,
    )?;
    Ok(out.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect())
}
```

**Existing rebase-state cleanup to call defensively** (`sync.rs:129-147`):
```rust
pub fn abort_stale_rebase(worktree: &Path) -> bool { ... }
```
Call before the loop AND on each rebase failure (RESEARCH Pitfall 4).

**Raw `Command::status()` precedent** for the `is-ancestor` probe (since `run_git_checked` collapses exit 1 vs 128 — RESEARCH Pitfall 1). Pattern shape from `gh_present` at `sync.rs:171-195`:
```rust
let mut cmd = Command::new("gh");
crate::common::process::hide_window(&mut cmd);
let child = cmd.arg("--version")
    .stdout(Stdio::null()).stderr(Stdio::null()).stdin(Stdio::null())
    .spawn();
```

**Core pattern (NEW reconcile helper):**
```rust
pub(crate) fn reconcile_against_remote(
    seed: &Path,
) -> Result<Vec<(String, ReconcileVerdict)>, GitError> {
    let _ = abort_stale_rebase(seed);
    git::run_git_checked(
        &["-C", &seed.to_string_lossy(), "fetch", "origin"],
        None, SYNC_TIMEOUT,
    )?;
    let mut verdicts = Vec::new();
    let branches = list_local_branches(seed)?;
    for branch in branches {
        let seed_s = seed.to_string_lossy().to_string();
        // remote-ref existence probe (REBASE_ABORT_TIMEOUT — fast, no I/O)
        let remote_ref = format!("refs/remotes/origin/{}", branch);
        let exists = git::run_git_checked(
            &["-C", &seed_s, "rev-parse", "--verify", "--quiet", &remote_ref],
            None, REBASE_ABORT_TIMEOUT,
        );
        if exists.is_err() {
            verdicts.push((branch, ReconcileVerdict::RemoteAbsentSafeToPush));
            continue;
        }
        // is-ancestor probe via raw Command::status to read exit 1 vs 128
        let mut cmd = std::process::Command::new("git");
        crate::common::process::hide_window(&mut cmd);
        let status = cmd
            .args(["-C", &seed_s,
                   "merge-base", "--is-ancestor",
                   &format!("origin/{}", branch), &branch])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .stdin(std::process::Stdio::null())
            .status();
        match status {
            Ok(s) if s.code() == Some(0) => {
                verdicts.push((branch, ReconcileVerdict::LocalAhead));
            }
            Ok(s) if s.code() == Some(1) => {
                let r = git::run_git_checked(
                    &["-C", &seed_s,
                      "rebase", "-X", "theirs",
                      &format!("origin/{}", branch), &branch],
                    None, SYNC_TIMEOUT,
                );
                match r {
                    Ok(_) => verdicts.push((branch, ReconcileVerdict::Reconciled)),
                    Err(e) => {
                        let _ = abort_stale_rebase(seed);
                        verdicts.push((branch, ReconcileVerdict::RebaseFailed(e)));
                    }
                }
            }
            _ => {
                verdicts.push((branch, ReconcileVerdict::ProbeFailed));
            }
        }
    }
    Ok(verdicts)
}
```

**Visibility:** `pub(crate)` per CLAUDE.md convention. `pull_branch` / `push_branch` use bare `pub` but reconcile is a setup-internal helper — `pub(crate)` matches `classify_and_record_outcome` at line 241.

---

### `src/common/sync.rs` — new `PerRefOutcome` / `ReconcileVerdict` types (model)

**Analog:** `src/common/sync.rs::SyncError` (`390-413`) — module-local enum, `#[derive(Debug)]`, hand-written `Display` impl.

**Existing pattern to mirror** (`sync.rs:390-413`):
```rust
#[derive(Debug)]
pub enum SyncError {
    GhMissing,
    GhAuthMissing,
    ScopeFallbackToBrowser,
    RepoCreateFailed(String),
    GitFailed(GitError),
    Io(std::io::Error),
}
impl std::fmt::Display for SyncError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SyncError::GhMissing => write!(f, "gh CLI not found on PATH"),
            // ...
        }
    }
}
```

**New types follow same shape:**
```rust
#[derive(Debug)]
pub enum ReconcileVerdict {
    RemoteAbsentSafeToPush,
    LocalAhead,
    Reconciled,
    RebaseFailed(GitError),
    ProbeFailed,
}

#[derive(Debug)]
pub struct PerRefOutcome {
    pub branch: String,
    pub reconcile: ReconcileVerdict,
    pub push: Option<Result<(), GitError>>,  // None iff push skipped (RebaseFailed)
}
```

**Visibility:** `pub` (not `pub(crate)`) because these appear in `accept_flow`'s return type, which is consumed by `src/owl/psyche_sync_setup.rs::run`. Same rationale as `SyncError`'s public visibility.

**No `Display` required** — dispatcher renders status tags by pattern-matching on the variant, not via `{}`. Skip the `Display` impl to keep surface minimal.

---

### `src/common/sync.rs::accept_flow` (controller, request-response — EDIT)

**Analog:** Self, `src/common/sync.rs:482-600`. The 6-step setup driver. Edit splices new logic between existing steps 4 and 5.

**Existing pre-push wiring to keep verbatim** (`sync.rs:485-559`): steps 1-4 (gh repo create, gh auth setup-git, build URL, per-worktree origin add+set-url).

**Existing step 5 to REPLACE** (`sync.rs:561-568`):
```rust
// Step 5 — single `git push --all origin` from seed/ (D-12 initial seeding).
let seed = owlery::seed_path();
git::run_git_checked(
    &["-C", &seed.to_string_lossy(), "push", "--all", "origin"],
    None, SYNC_TIMEOUT,
)
.map_err(SyncError::GitFailed)?;
```

**NEW step 4b — wire origin on the seed** (RESEARCH Pitfall 6 — uses same add-then-set-url idempotent pattern as worktree loop at `sync.rs:535-558`):
```rust
let seed = owlery::seed_path();
let _ = git::run_git_checked(
    &["-C", &seed.to_string_lossy(), "remote", "add", "origin", &remote_url],
    None, REBASE_ABORT_TIMEOUT,
);
let _ = git::run_git_checked(
    &["-C", &seed.to_string_lossy(), "remote", "set-url", "origin", &remote_url],
    None, REBASE_ABORT_TIMEOUT,
);
```

**NEW step 4c — reconcile** (replaces nothing; pure insertion):
```rust
let verdicts = reconcile_against_remote(&seed)
    .map_err(SyncError::GitFailed)?;
```

**NEW step 5 — per-ref push loop** (REPLACES old `push --all`):
```rust
let mut outcomes: Vec<PerRefOutcome> = Vec::with_capacity(verdicts.len());
for (branch, verdict) in verdicts {
    // Skip push when reconciliation hard-failed (preserves remote state).
    let push_result = match &verdict {
        ReconcileVerdict::RebaseFailed(_) => None,
        _ => {
            let refspec = format!("{}:{}", branch, branch);
            Some(
                git::run_git_checked(
                    &["-C", &seed.to_string_lossy(),
                      "push", "origin", &refspec],
                    None, SYNC_TIMEOUT,
                ).map(|_| ()),
            )
        }
    };
    outcomes.push(PerRefOutcome { branch, reconcile: verdict, push: push_result });
}
```

**Existing step 5b (per-branch `--set-upstream-to`) — KEEP** (`sync.rs:570-586`), no change.

**Existing step 6 (settings persist) — KEEP** (`sync.rs:588-597`), but the function's return becomes a tuple:

**Return-shape change** (`sync.rs:482`):
```rust
// before
pub fn accept_flow(user: &str) -> Result<String, SyncError> { ... Ok(remote_url) }
// after
pub fn accept_flow(user: &str) -> Result<(String, Vec<PerRefOutcome>), SyncError> {
    ...
    Ok((remote_url, outcomes))
}
```

**Soft-fail policy preserved:** the reconcile error path uses `SyncError::GitFailed(GitError)` (existing variant — line 396); no new error variant required. A push failure on a single branch becomes an `Err` *inside* `outcomes`, not a top-level `Err` — `accept_flow` itself only short-circuits when fetch fails (network) or settings.json write fails (disk).

---

### `src/owl/psyche_sync_setup.rs::run` (controller / dispatch — EDIT)

**Analog:** Self, `src/owl/psyche_sync_setup.rs:61-73` — the existing `match accept_flow` arms.

**Existing match arms** (`psyche_sync_setup.rs:61-73`):
```rust
match sync::accept_flow(&user) {
    Ok(url) => println!("sync enabled; remote={}", url),
    Err(sync::SyncError::ScopeFallbackToBrowser) => {
        println!("gh token missing 'repo' scope. ...");
        std::process::exit(5);
    }
    Err(e) => {
        eprintln!("sync setup failed: {:?}", e);
        std::process::exit(1);
    }
}
```

**Edit pattern — update the `Ok` arm to destructure the new tuple + emit per-ref tags:**
```rust
match sync::accept_flow(&user) {
    Ok((url, outcomes)) => {
        // Per-ref status tags on stderr (CLAUDE.md convention: status to stderr).
        for o in &outcomes {
            emit_outcome_tag(o);
        }
        println!("sync enabled; remote={}", url);
    }
    // ... other arms unchanged
}
```

**NEW dispatcher helper** (new fn in the same file — module-private):
```rust
fn emit_outcome_tag(o: &sync::PerRefOutcome) {
    use sync::ReconcileVerdict::*;
    // Reconcile-side tag
    match &o.reconcile {
        Reconciled => eprintln!("RECONCILED:{}", o.branch),
        RebaseFailed(_) => eprintln!("DIVERGED:{}", o.branch),
        _ => {}  // RemoteAbsentSafeToPush / LocalAhead / ProbeFailed are silent here
    }
    // Push-side tag
    match &o.push {
        Some(Ok(())) => eprintln!("PUSHED:{}", o.branch),
        Some(Err(_)) => eprintln!("PUSH_FAILED:{}", o.branch),
        None => {}  // RebaseFailed already reported DIVERGED
    }
}
```

**Output convention (CLAUDE.md line 56):** Status tags on **stderr**. Owl cyan color is owned by the existing print path — the new tags inherit the dispatcher's existing stderr style (no ANSI wrapper needed at the tag-emit site; downstream colorizer if any picks up the tag prefix). RESEARCH Pattern 4 aligns: sync is setup-side → owl cyan tone.

**Note on Issue 4 (`{:?}` Debug repr leak) at line 70:** RESEARCH explicitly defers this to Phase 35.3. The line stays `{:?}` for 35.2.

---

### `tests/sync_two_machine_attach.rs` (NEW — cross-platform test)

**Analog:** `tests/sync_pull_push.rs` (full file) — header/import/serial/SptHomeGuard shape.

**Header pattern** (`tests/sync_pull_push.rs:18-25`):
```rust
#[path = "common/sync_fixtures.rs"]
mod sync_fixtures;

use owl::common::sync;
use sync_fixtures::{
    clone_from, commit_file, current_branch, git_available, git_ok, git_stdout,
    init_fake_bare_remote, read_normalized, SptHomeGuard,
};
```

**Serial+guard+git-probe shape** (`tests/sync_pull_push.rs:27-35`):
```rust
#[test]
#[serial_test::serial]
fn sync_pull_push_round_trip() {
    if !git_available() {
        eprintln!("skip sync_pull_push_round_trip: git not on PATH");
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let _home = SptHomeGuard::set(tmp.path());
    // ...
}
```

**Core test pattern (bootstrap determinism):**
```rust
#[test]
#[serial_test::serial]
fn two_machines_bootstrap_to_identical_main_sha() {
    if !git_available() { eprintln!("skip: git not on PATH"); return; }
    let tmp = tempfile::tempdir().unwrap();

    let sha_a = {
        let _g = SptHomeGuard::set(&tmp.path().join("a"));
        owl::common::tracked::ensure_seed().unwrap();
        git_stdout(&owl::common::owlery::seed_path(),
                   &["rev-parse", "refs/heads/main"])
    };
    let sha_b = {
        let _g = SptHomeGuard::set(&tmp.path().join("b"));
        owl::common::tracked::ensure_seed().unwrap();
        git_stdout(&owl::common::owlery::seed_path(),
                   &["rev-parse", "refs/heads/main"])
    };
    assert_eq!(sha_a, sha_b,
        "deterministic bootstrap: identical main SHA across SPT_HOME roots");
}
```

---

### `tests/sync_reconcile_against_remote.rs` (NEW — cross-platform)

**Analog:** `tests/sync_pull_push.rs` for shape; `sync_fixtures::init_fake_bare_remote` + `commit_file` for fixture setup.

**Three sub-cases** matching RESEARCH coverage matrix:
- `is_ancestor_probe_routes_branches_correctly` — ancestor (ahead), not-ancestor (diverged), missing-remote-ref.
- `diverged_branch_rebases_with_theirs_strategy` — asserts post-rebase HEAD contains both A and B commit subjects.
- `fetch_runs_before_push_when_remote_exists` — assert verdicts list lengths.

**Reuses helpers verbatim** (`tests/common/sync_fixtures.rs:62-94`): `git_run`, `git_ok`, `git_stdout`, `init_fake_bare_remote`, `commit_file`.

---

### `tests/sync_accept_flow_two_machine.rs` (NEW — `#[cfg(unix)]`)

**Analog:** `src/common/sync.rs::tests::fake_gh` module (`sync.rs:1226-1401`).

**Reuse pattern verbatim** — `PathGuard`, `write_script`, `make_seed` helpers. Per RESEARCH §Test Scaffolding: either inline these in the new test file or promote behind `#[cfg(unix)]` to `sync_fixtures.rs`. Recommend inline (small, Unix-gated) for 35.2.

**Existing pattern** (`sync.rs:1255-1270`):
```rust
fn write_script(dir: &std::path::Path, name: &str, body: &str) {
    let path = dir.join(name);
    std::fs::write(&path, body).unwrap();
    let mut perms = std::fs::metadata(&path).unwrap().permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&path, perms).unwrap();
}
```

**Cross-machine assertion** (RESEARCH §Code Examples lines 539-584):
```rust
// Machine A pushes; capture remote a-doyle SHA.
// Machine B's SPT_HOME is a separate dir, runs accept_flow against same bare.
// Post-fix: A's commit must still appear in `git log a-doyle --oneline` on bare.
let log_after = git_stdout(&bare, &["log", "a-doyle", "--oneline"]);
assert!(log_after.contains(&a_head_after[..8]),
    "machine A's commit must survive machine B attach; got: {log_after}");
```

---

### `CLAUDE.md` — status tag inventory append

**Analog:** Line 56 (existing inventory):
```markdown
- Status tags: `READY:id`, `SENT:id`, `STOPPED:id`, `CLEANED:id`, `SOFT-CLEANED:id`. Errors: `NO_PERCH:id`, `STALE:id`, `DUPLICATE:id`, `COLLISION:id`.
```

**Append pattern (new bullet, one line under existing):**
```markdown
- Sync status tags (psyche-sync-setup per-ref): `PUSHED:{branch}`, `RECONCILED:{branch}`. Errors: `DIVERGED:{branch}`, `PUSH_FAILED:{branch}`.
```

(Brief one-liner matches the inventory style — no expanded paragraph. Planner Phase 35.1 Plan 04 set this precedent per RESEARCH §Open Q 4.)

---

## Shared Patterns

### Pattern S-1: `hide_window` on every `Command::new("git")` / `Command::new("gh")`

**Source:** `src/common/sync.rs:174, 488, 519`; `src/owl/psyche_sync_setup.rs:93, 107`; `src/common/git.rs:485`.

**Apply to:** Every new direct `Command::new(...)` invocation (the `commit-tree` env-vars site in `ensure_seed`, the `is-ancestor` probe in `reconcile_against_remote`).

```rust
let mut cmd = std::process::Command::new("git");
crate::common::process::hide_window(&mut cmd);
```

Windows-portability invariant per CLAUDE.md "Windows native + Unix".

### Pattern S-2: Stdio nulling on probes that don't consume output

**Source:** `src/common/sync.rs:177-179` (`gh_present`), `src/owl/psyche_sync_setup.rs:95-97` (`check_gh_auth`).

**Apply to:** The `is-ancestor` probe (we want exit code only, not stderr noise).

```rust
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
```

### Pattern S-3: `run_git_checked` routing for every recoverable git call

**Source:** Used pervasively across `sync.rs` (every helper except the two raw-`Command` probes) and `tracked.rs` (every `git` step in `ensure_seed`).

**Apply to:** Every new git invocation EXCEPT (a) the `ensure_seed` `commit-tree` (needs env vars — bypass), (b) the `is-ancestor` probe (needs raw exit code — bypass per RESEARCH Pitfall 1). All other new git calls (fetch, rebase, rev-parse --verify, push origin B:B) MUST route through `run_git_checked` with explicit `SYNC_TIMEOUT` or `REBASE_ABORT_TIMEOUT`.

```rust
git::run_git_checked(&["-C", &p.to_string_lossy(), ...], None, SYNC_TIMEOUT)
```

### Pattern S-4: `SptHomeGuard` + `serial_test::serial` for every settings-touching test

**Source:** `tests/common/sync_fixtures.rs:27-46`; `tests/sync_pull_push.rs:28-35`; `src/common/sync.rs:634-655` (private duplicate inside in-module tests).

**Apply to:** Every new integration test (`sync_two_machine_attach.rs`, `sync_reconcile_against_remote.rs`, `sync_accept_flow_two_machine.rs`).

```rust
#[test]
#[serial_test::serial]
fn my_test() {
    if !git_available() { eprintln!("skip: git not on PATH"); return; }
    let tmp = tempfile::tempdir().unwrap();
    let _home = SptHomeGuard::set(tmp.path());
    // ...
}
```

### Pattern S-5: ANSI-color stderr convention (CLAUDE.md §Conventions)

**Source:** CLAUDE.md line 55 — "Output: status → stderr (ANSI-colored: owl cyan, live orange), message body → stdout."

**Apply to:** Dispatcher status-tag emit in `psyche_sync_setup.rs`. Sync is setup-flow (owl-side, not live-side) → cyan tone. RESEARCH §Pattern 4 final paragraph confirms this allocation.

The existing `eprintln!` sites in `psyche_sync_setup.rs:32-72` are plain `eprintln!` (no explicit ANSI). New tag emits follow the same plain-`eprintln!` convention — any downstream colorizer keys off the `TAG:` prefix.

### Pattern S-6: `pub(crate)` for cross-module helpers, `pub` only when types/fns escape to dispatcher

**Source:** CLAUDE.md §Conventions; `sync.rs:241` (`classify_and_record_outcome` is `pub(crate)`); `sync.rs:482` (`accept_flow` is `pub` — escapes to `psyche_sync_setup.rs`).

**Apply to:**
- `reconcile_against_remote` → `pub(crate)` (internal helper).
- `push_per_ref` if extracted → `pub(crate)` (internal helper).
- `PerRefOutcome`, `ReconcileVerdict` → `pub` (appear in `accept_flow`'s public return type).

---

## No Analog Found

None. Every new symbol in Phase 35.2 has a direct precedent inside `src/common/sync.rs` or `src/common/tracked.rs`:

| Need | Direct in-tree precedent |
|------|--------------------------|
| Env-var on a git Command | None for `git`; close analog: `gh_present`'s raw `Command::new("gh")` at `sync.rs:171-195` |
| Raw `Command::status()` exit-code read | None on git; the `gh_present` probe uses `try_wait` (semantically similar) |
| Per-branch fan-out loop | `enumerate_existing_worktrees` + the worktree origin-add loop at `sync.rs:534-559` |
| Result-vec capture across loop | None in `src/`; closest is `verdicts` Vec shape in research pseudocode |

The two "raw `Command`" sites (commit-tree env-vars; is-ancestor exit-code probe) are documented bypasses of `run_git_checked` with explicit rationale comments per RESEARCH Pitfalls 1 and Pattern 3 Option B.

---

## Metadata

**Analog search scope:**
- `src/common/sync.rs` (full file — 1400+ lines, targeted reads: 1-340, 440-720, 720-1020, 1218-1401)
- `src/common/tracked.rs` (lines 180-460 — ensure_seed + ensure_worktree)
- `src/common/git.rs` (lines 436-545 — GitError + run_git_checked)
- `src/owl/psyche_sync_setup.rs` (full file — 176 lines)
- `tests/common/sync_fixtures.rs` (full file — 179 lines)
- `tests/sync_pull_push.rs` (full file — 103 lines, shape analog)
- `CLAUDE.md` line 56 (status-tag inventory)

**Files scanned:** 7 source files + 1 test fixture + 1 integration test + CLAUDE.md
**Pattern extraction date:** 2026-05-28
**Constraints honored (CLAUDE.md):**
- Windows + Unix portability — `hide_window` on every new Command site
- Zero new deps — all patterns use existing in-tree helpers
- `pub(crate)` for cross-module, `pub` only for return-type escape
- Status tags on stderr (owl cyan tone)
- All git work routes through `run_git_checked` except two documented bypasses (env vars / raw exit code)
