---
phase: 35.2-psyche-sync-setup-data-loss-reconcile-bootstrap-determinism
plan: 03
subsystem: sync
tags: [sync, accept_flow, dispatcher, status-tags]
requires:
  - sync::reconcile_against_remote (Plan 35.2-02)
  - sync::ReconcileVerdict (Plan 35.2-02)
  - sync::PerRefOutcome (Plan 35.2-02)
  - tracked::ensure_seed / ensure_agent_worktree
provides:
  - sync::accept_flow returns (String, Vec<PerRefOutcome>)
  - psyche_sync_setup::emit_outcome_tag (per-ref status tags on stderr)
affects:
  - DEPLOY (closes Phase 35.2 data-loss surface alongside 35.2-01/02)
tech-stack:
  added: []
  patterns:
    - per-ref refspec push (push origin {b}:{b}) replacing push --all
    - GIT_CONFIG_GLOBAL insteadOf rewrite for fake-remote end-to-end tests
key-files:
  created:
    - tests/sync_accept_flow_two_machine.rs
  modified:
    - src/common/sync.rs
    - src/owl/psyche_sync_setup.rs
    - CLAUDE.md
decisions:
  - "Tasks 1-3 committed as one atomic commit (signature change is a build-break until the dispatcher updates; plan success-criteria mandates a single atomic sync commit). Task 4 (test) committed separately."
  - "In-module Test 6 retargeted from `push --all` to per-ref push (the asserted behavior was intentionally removed by this plan) — Rule 1 test correction."
  - "End-to-end test routes the canonical HTTPS URL to the local bare remote via a per-test GIT_CONFIG_GLOBAL insteadOf rewrite, so accept_flow's own seed set-url cannot defeat the fixture wiring."
metrics:
  duration: ~14min
  completed: 2026-05-28
---

# Phase 35.2 Plan 03: accept_flow pre-push reconcile + per-ref outcomes Summary

Composed the Plan 35.2-02 reconcile primitive at the `accept_flow` call site: the single `git push --all origin` is replaced by a fetch-and-reconcile pass followed by a per-branch `push origin {b}:{b}` loop that skips any branch whose reconcile hard-failed. `accept_flow` now returns `(String, Vec<PerRefOutcome>)`; the `psyche_sync_setup::run` dispatcher destructures the tuple and renders `PUSHED` / `RECONCILED` / `DIVERGED` / `PUSH_FAILED` status tags on stderr via a new private `emit_outcome_tag`. A Unix-gated end-to-end test drives the full `accept_flow` path against a fake-gh + local bare remote and asserts machine A's writes survive machine B's attach. This plan completes Phase 35.2's data-loss-elimination surface.

## Signature Change (before/after)

| | Signature |
|---|---|
| Before | `pub fn accept_flow(user: &str) -> Result<String, SyncError>` |
| After  | `pub fn accept_flow(user: &str) -> Result<(String, Vec<PerRefOutcome>), SyncError>` |

Final return changed `Ok(remote_url)` → `Ok((remote_url, outcomes))`. Short-circuit error paths (gh-create scope-fallback, repo-create-failed, reconcile fetch failure → `GitFailed`, settings write → `Io`) are unchanged.

## accept_flow body changes

- **Step 4b (new):** seed-origin wire — `git -C {seed} remote add origin {url}` + `remote set-url origin {url}`, both `let _ =` soft-fail (Pitfall 6). Mirrors the existing worktree add+set-url idempotent pattern.
- **Step 4c (new):** `let verdicts = reconcile_against_remote(&seed).map_err(SyncError::GitFailed)?;`
- **Step 5 (replaced):** per-branch loop. `RebaseFailed(_)` → `push: None` (skip, preserve remote). Else `git push origin {branch}:{branch}` via `run_git_checked` (SYNC_TIMEOUT) captured into `Some(Result)`. Each `(branch, verdict, push)` pushed onto `Vec<PerRefOutcome>`.
- Steps 1/2/3/4 (gh create, setup-git, URL, worktree origin loop), 5b (`--set-upstream-to`), 6 (settings persist) unchanged.

## Status-tag emit sites (Task 2)

`src/owl/psyche_sync_setup.rs::emit_outcome_tag` (module-private) — four `eprintln!` sites, branch-name-only surfaces:

| Tag | Trigger | Branch surface |
|-----|---------|----------------|
| `RECONCILED:{branch}` | `ReconcileVerdict::Reconciled` | `o.branch` |
| `DIVERGED:{branch}` | `ReconcileVerdict::RebaseFailed(_)` | `o.branch` |
| `PUSHED:{branch}` | `push == Some(Ok(()))` | `o.branch` |
| `PUSH_FAILED:{branch}` | `push == Some(Err(_))` | `o.branch` |

Quiet variants (`RemoteAbsentSafeToPush` / `LocalAhead` / `ProbeFailed`, and `push: None`) emit nothing. The catch-all `Err(e) => eprintln!("sync setup failed: {:?}", e)` Debug-repr is preserved (Phase 35.3 scope).

## CLAUDE.md

New inventory line landed at **line 57** (immediately after the existing status-tag line 56) under "Conventions":
`- Sync status tags (psyche-sync-setup per-ref): `PUSHED:{branch}`, `RECONCILED:{branch}`. Errors: `DIVERGED:{branch}`, `PUSH_FAILED:{branch}`.`

## Test outcomes

| Test surface | Windows (build machine) | Unix (CI/build machine) |
|--------------|-------------------------|--------------------------|
| `tests/sync_accept_flow_two_machine.rs` | 0 tests (file-level `#![cfg(unix)]` gate; build green) | 1 test — `two_machine_attach_preserves_first_machine_writes` (asserts A's full-`%H` SHA + "A: foo" subject survive in bare `a-doyle` after B's attach) |
| `src/common/sync.rs` in-module (`cargo test --lib common::sync`) | 23/23 ok | (unix adds fake_gh tests) |
| `psyche_sync_setup` dispatcher (`disable_flips_state_failing_with_reason`) | 1/1 ok | 1/1 ok |
| Prior-wave `sync_reconcile_against_remote` | 3/3 ok | 3/3 ok |
| Prior-wave `sync_two_machine_attach` | 1/1 ok | 1/1 ok |

`cargo build --release` exits 0 with no new warnings on any modified file (only pre-existing unrelated dead-code warnings).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] In-module Test 6 (`push_all_from_seed`) asserted removed behavior**
- **Found during:** Task 1
- **Issue:** Test 6 asserted the fake git observed a `push --all` invocation from the seed. This plan intentionally removes `push --all` in favor of per-ref `push origin {b}:{b}`, so the test would assert against deleted behavior. Additionally its fully-faked git (`exit 0` with no stdout) makes `list_local_branches` return empty, so the new per-ref loop would never push.
- **Fix:** Renamed to `push_per_ref_from_seed`; reworked the fake git to emit one branch (`a-doyle`) for `branch --format`, fail the remote-ref `rev-parse` (→ `RemoteAbsentSafeToPush`, so reconcile does not skip), and log the `-C` dir on any `push` subcommand. Assertion now proves the per-ref push runs from `seed_path()`.
- **Files modified:** `src/common/sync.rs`
- **Commit:** 6ffe93c

**2. [Rule 3 - Blocking] End-to-end fixture URL routing**
- **Found during:** Task 4
- **Issue:** `accept_flow` step 4b wires the seed origin to the canonical HTTPS URL via `remote set-url`, which would clobber any pre-wired `file://` bare-remote origin, so fetch/push could never reach the test's local bare repo.
- **Fix:** Per-test `GIT_CONFIG_GLOBAL` guard pointing at a temp config carrying `[url "file://<bare>"] insteadOf = https://github.com/testuser/spt-agent-storage.git` (plus synthetic identity). accept_flow's HTTPS URL transparently rewrites to the bare remote; the guard restores `GIT_CONFIG_GLOBAL` on drop so nothing leaks. This keeps the test driving `accept_flow` end-to-end (no direct `reconcile_against_remote` call — negative acceptance preserved).
- **Files modified:** `tests/sync_accept_flow_two_machine.rs`
- **Commit:** 52e9d5e

## Threat Flags

None — no new security surface beyond the threat register in PLAN.md. Per-ref refspec is built from git-validated branch names; status tags emit branch names only (no SHA/token/stderr).

## Commits

- `6ffe93c` fix(sync): accept_flow pre-push reconcile + per-ref outcomes (SYNC-PUSH-PER-REF-01) — Tasks 1, 2, 3
- `52e9d5e` test(35.2-03): unix-gated end-to-end accept_flow two-machine attach test — Task 4

## Self-Check: PASSED

- FOUND: `tests/sync_accept_flow_two_machine.rs`
- FOUND: `.planning/phases/35.2-.../35.2-03-SUMMARY.md`
- FOUND commits: 6ffe93c, 52e9d5e
