---
phase: 35.2-psyche-sync-setup-data-loss-reconcile-bootstrap-determinism
plan: 02
subsystem: sync
tags: [sync, reconcile, git]
requires:
  - tracked::ensure_seed (deterministic seed — Plan 35.2-01)
  - tracked::ensure_agent_worktree
  - git::run_git_checked
  - sync::abort_stale_rebase
  - sync::list_local_branches
provides:
  - sync::reconcile_against_remote
  - sync::ReconcileVerdict
  - sync::PerRefOutcome
  - sync::worktree_for_branch (private)
affects:
  - accept_flow wire-up (Plan 35.2-03 — NOT touched here)
tech-stack:
  added: []
  patterns:
    - raw Command::status() exit-code probe (Pitfall 1 bypass of run_git_checked)
    - worktree-scoped rebase (git rebase requires a work tree)
key-files:
  created:
    - tests/sync_reconcile_against_remote.rs
  modified:
    - src/common/sync.rs
decisions:
  - "reconcile_against_remote is bare `pub` (not `pub(crate)`): integration tests cannot reach pub(crate) items (E0603); matches sibling pull_branch/push_branch."
  - "Rebase runs in the branch's linked worktree, not the bare seed: `git rebase` cannot run in a bare repo. Plan/RESEARCH asserted 'rebase on bare seed with explicit branch positional' — factually wrong; corrected."
  - "A diverged branch with no resolvable worktree routes to ProbeFailed (cannot rebase without a checkout)."
metrics:
  duration: ~12min
  completed: 2026-05-28
---

# Phase 35.2 Plan 02: reconcile_against_remote primitive Summary

Introduced the `reconcile_against_remote` reconcile primitive plus the `ReconcileVerdict` enum and `PerRefOutcome` struct into `src/common/sync.rs`, with a cross-platform routing test matrix — fetch-once then per-branch route to LocalAhead / Reconciled / RemoteAbsentSafeToPush via a raw `merge-base --is-ancestor` exit-code probe, with worktree-scoped `rebase -X theirs` for diverged branches. `accept_flow` is unchanged (Plan 35.2-03 owns the wire-up).

## New Symbols Added

| Symbol | Kind | Location | Visibility |
|--------|------|----------|------------|
| `ReconcileVerdict` | enum (5 variants) | `src/common/sync.rs:424` | `pub` |
| `PerRefOutcome` | struct (3 fields) | `src/common/sync.rs:454` | `pub` |
| `worktree_for_branch` | fn | `src/common/sync.rs:555` | private (module) |
| `reconcile_against_remote` | fn | `src/common/sync.rs:592` | `pub` (see deviation) |

`ReconcileVerdict` variants: `RemoteAbsentSafeToPush`, `LocalAhead`, `Reconciled`, `RebaseFailed(GitError)`, `ProbeFailed`. No `Display` impl (dispatcher pattern-matches — RESEARCH Pattern 4).

`PerRefOutcome` fields: `pub branch: String`, `pub reconcile: ReconcileVerdict`, `pub push: Option<Result<(), GitError>>`.

## Test Outcomes

`tests/sync_reconcile_against_remote.rs` — 3 cross-platform `#[serial_test::serial]` tests, all **pass** on the build machine (Windows):

| Test | Result | Asserts |
|------|--------|---------|
| `is_ancestor_probe_routes_branches_correctly` | ok | a-alpha==origin → LocalAhead; a-beta never pushed → RemoteAbsentSafeToPush; diverged a-alpha → Reconciled |
| `diverged_branch_rebases_with_theirs_strategy` | ok | machine-B a-doyle → Reconciled; post-rebase `git log a-doyle` contains BOTH "A: foo" and "B: bar" (A's work survives) |
| `fetch_runs_before_push_when_remote_exists` | ok | verdicts.len() == local-branch count; a-one (pushed) → LocalAhead; a-two (unpushed) → RemoteAbsentSafeToPush |

Full sync lib suite (single-threaded): `cargo test --lib common::sync -- --test-threads=1` → **23/23 ok** (no regressions). `cargo build --release` exits 0 (only pre-existing unrelated dead-code warnings).

## `accept_flow` Confirmation

`accept_flow` source is **unchanged**. `git diff HEAD -- src/common/sync.rs` shows only new additions (the two types + the helper + `worktree_for_branch`); the only `+` line mentioning `accept_flow` is the new helper's doc-comment reference. No logic-line changes to the accept_flow body. Plan 35.2-03 owns the wire-up.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Rebase must run in a worktree, not the bare seed**
- **Found during:** Task 3 (test execution)
- **Issue:** The PLAN action (Task 2 step 9), RESEARCH ("rebase on bare seed with explicit branch positional", line 505), and PATTERNS.md all directed `git -C {seed} rebase ...`. The seed is created via `git init --bare` (`tracked.rs:209`) and has NO working tree, so `git rebase` fails immediately with `fatal: this operation must be run in a work tree`. The is-ancestor / fetch / rev-parse probes work fine in a bare repo; only rebase does not. Two tests routed diverged branches to `RebaseFailed(...)` instead of `Reconciled`.
- **Fix:** Added private `worktree_for_branch(seed, branch)` that parses `git -C {seed} worktree list --porcelain` to resolve the linked worktree holding `refs/heads/{branch}`, and runs the `rebase -X theirs origin/{branch} {branch}` (plus the defensive `abort_stale_rebase`) inside that worktree (`-C {worktree}`). A branch with no resolvable worktree (e.g. the seed's own bare `main`) routes to `ProbeFailed`. The algorithm, routing matrix, verdict types, and public signature are all unchanged — only the rebase execution location is corrected.
- **Files modified:** `src/common/sync.rs`
- **Commit:** 622b721

**2. [Rule 3 - Blocking] `reconcile_against_remote` visibility `pub(crate)` → `pub`**
- **Found during:** Task 3 (test compile)
- **Issue:** PLAN/PATTERNS specified `pub(crate)` AND required the integration test (`tests/sync_reconcile_against_remote.rs`, Behavior 4) to call `owl::common::sync::reconcile_against_remote` directly. The `tests/` crate is an external consumer of the `owl` library; `pub(crate)` items are not reachable from it (E0603). The two requirements were mutually exclusive.
- **Fix:** Made `reconcile_against_remote` bare `pub`, matching its already-`pub` siblings `pull_branch` / `push_branch` (which are likewise exercised cross-crate by `tests/sync_pull_push.rs`). Intent remains a setup-internal helper.
- **Files modified:** `src/common/sync.rs`
- **Commit:** 622b721

### Affected Acceptance Criteria

- Task 2 acceptance criterion `grep -c 'abort_stale_rebase(seed)' >= 2` now returns 1 (pre-loop call). The post-failure abort now correctly targets the **worktree** (`abort_stale_rebase(&wt)`), not the seed — the defensive pre-loop + post-failure intent (Pitfall 4) is fully preserved, just scoped to the correct work tree. This criterion was predicated on the disproven "rebase in seed" assumption.

## Out-of-Scope Discoveries (logged, NOT fixed)

See `deferred-items.md`:
- `tests/native_wrapper_state_retry.rs` fails to compile (`pulse_psyche` field missing — pre-existing, unrelated test crate).
- ~21 parallel-test failures (process-global `SPT_HOME` collisions — documented pre-existing issue; all pass in isolation).

## Commits

- `7d16878` feat(35.2-02): add ReconcileVerdict enum + PerRefOutcome struct to sync.rs
- `2da0061` feat(35.2-02): implement reconcile_against_remote helper in sync.rs
- `622b721` test(35.2-02): cross-platform routing tests + worktree-scoped rebase fix

## Self-Check: PASSED

- FOUND: `tests/sync_reconcile_against_remote.rs`
- FOUND: `.planning/phases/35.2-.../35.2-02-SUMMARY.md`
- FOUND commits: 7d16878, 2da0061, 622b721
