---
phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo
plan: 04
subsystem: psyche-sync
tags: [sync, gh-cli, setup, clap, skill, accept-flow]
requires:
  - 35-01  # SyncSettings/SyncState data layer + sync_settings_path
  - 35-02  # pull_branch/push_branch/maybe_add_origin/gh_present/SYNC_TIMEOUT
provides:
  - "sync::accept_flow(user) -> Result<String, SyncError>"
  - "sync::SyncError enum"
  - "sync::enumerate_existing_worktrees()"
  - "sync::run_async_pull_for_agent_and_project(agent, project)"
  - "Commands::SyncPullAsync { agent, project }"
  - "Commands::PsycheSyncSetup { disable }"
  - "owl::psyche_sync_setup::run(disable)"
  - "plugin/spt/skills/psyche-sync-setup/SKILL.md"
affects:
  - 35-05  # UserPromptSubmit dispatch consumes `owl sync-pull-async`
  - 35-07  # SessionStart prompt routes to accept_flow via psyche-sync-setup
  - 35-06  # maybe_add_origin lazy-inherit for new worktrees (complements accept_flow seeding)
  - 35-08  # doctor surfaces sync.state / remote_url / acked_ts
tech-stack:
  added: []        # no new crate deps; gh + git are system CLIs (setup-only)
  patterns:
    - "gh shelled via std::process::Command (NOT run_git_checked — gh is not git)"
    - "process::hide_window applied to every gh Command (Windows console-flash guard)"
    - "fake-gh PATH-override tests #[cfg(unix)]-gated (mirrors golden_*.rs Unix gating)"
key-files:
  created:
    - src/owl/psyche_sync_setup.rs
    - plugin/spt/skills/psyche-sync-setup/SKILL.md
  modified:
    - src/common/sync.rs
    - src/cli.rs
    - src/owl/mod.rs
decisions:
  - "Remote URL is HTTPS (https://github.com/{user}/spt-agent-storage.git), not SSH — RESEARCH §Open Q 2 researcher recommendation; no key-management prereq for the user"
  - "plugin.json does NOT enumerate skills (auto-discovered) — Action step E was a verified no-op"
  - "53 full-suite parallel-test failures are pre-existing SPT_HOME-global collisions (PROJECT.md backlog); all 20 plan tests pass single-threaded — NOT a regression"
metrics:
  duration: 442s
  completed: 2026-05-26
  tasks: 2
  files: 5
---

# Phase 35 Plan 04: Unified Sync Setup Front Door Summary

One-liner: Wired `accept_flow` (the D-10 six-step `gh repo create` → seed-push setup driver) plus two clap subcommands (`sync-pull-async`, `psyche-sync-setup`) and a unified `/spt:psyche-sync-setup` skill, giving end users a one-shot path to a fully-wired private cross-machine context-backup remote.

## What Shipped

**Task 1 — `src/common/sync.rs`** (commit `8dc1ad3`):
- `pub enum SyncError` — `GhMissing`, `GhAuthMissing`, `ScopeFallbackToBrowser`, `RepoCreateFailed(String)`, `GitFailed(GitError)`, `Io(std::io::Error)`; `Debug` + per-variant `Display`.
- `pub fn enumerate_existing_worktrees() -> Vec<PathBuf>` — walks `tracked/agents/*` + `tracked/projects/*` for `.git`-carrying dirs (dir OR linked-worktree file).
- `fn list_local_branches(seed)` — `git branch --format=%(refname:short)`, empty-filtered.
- `pub fn accept_flow(user) -> Result<String, SyncError>` — the D-10 sequence: gh repo create (scope-fallback branch) → `gh auth setup-git` once (Pattern 6) → HTTPS URL → per-worktree `remote add origin` (soft-fail) → single `git push --all origin` from `seed/` (D-12/Pitfall 6) → per-branch `--set-upstream-to` (Pitfall 4) → persist `state=Enabled` + remote_url + acked_ts (D-10 step 5).
- `pub fn run_async_pull_for_agent_and_project(agent, project)` — gated fire-and-forget pull (D-05/D-14) backing `sync-pull-async`.
- 8 behavior tests: 4 cross-platform (`enumerate_worktrees`, `list_local_branches`, two `async_pull` no-op gates), 4 Unix-gated fake-gh (`accept_flow_happy`, `accept_flow_scope_insufficient`, `setup_git_invoked_once`, `push_all_from_seed`).

**Task 2 — CLI + dispatch + module + skill** (commit `5f8ffed`):
- `src/cli.rs`: `SyncPullAsync { agent: String, project: Option<String> }` + `PsycheSyncSetup { disable: bool }`.
- `src/owl/mod.rs`: `pub mod psyche_sync_setup;` + two dispatch arms.
- `src/owl/psyche_sync_setup.rs`: `pub fn run(disable)` — gh-present (exit 2) + gh-auth (exit 3) probes, idempotent already-enabled status report, `probe_gh_user` (exit 4), `accept_flow` invocation, scope-fallback browser URL (exit 5), `--disable` → `state=Failing` + `user-disabled` (D-19). 1 cross-platform test (`disable_flips_state_failing_with_reason`).
- `plugin/spt/skills/psyche-sync-setup/SKILL.md`: D-13 unified front door; sections Prerequisites / Authentication / Run setup / Already enabled? / To disable / Caveats.

## Output-Spec Items (requested by PLAN `<output>`)

**1. Exact AskUserQuestion phrasings shipped in SKILL.md** (RESEARCH §Open Q 4 deferred to plan-checker):

- *Install prompt* (when `gh --version` fails):
  > "The `gh` CLI is required to set up cross-machine sync but isn't installed. How would you like to install it?"
  > Options: **winget** (`winget install GitHub.cli`) / **brew** (`brew install gh`) / **apt** (`sudo apt install gh`) / **Show download URL** (https://cli.github.com/) / **Cancel**.

- *Scope-fallback prompt* (binary exit code 5):
  > "Your `gh` token can't create private repos (missing 'repo' scope). How do you want to proceed?"
  > Options: **Refresh scope** (`gh auth refresh --scopes repo`) / **Create in browser** (https://github.com/new?name=spt-agent-storage&visibility=private) / **Cancel**.

Authentication is an interactive `gh auth login` Bash step (not AskUserQuestion) — user must select the **'repo'** scope at the gh prompt.

**2. HTTPS-vs-SSH remote URL choice + rationale:** HTTPS — `https://github.com/{user}/spt-agent-storage.git`. Per RESEARCH §Open Q 2 the researcher recommended HTTPS over SSH: `gh auth setup-git` configures the gh credential helper for HTTPS, so no SSH-key management is required of the user. Plan-checker did not override.

**3. Vocabulary grep-gate command + final count:**
- Command: `Grep -i '\blistener\b' plugin/spt/skills/psyche-sync-setup/SKILL.md`
- Final count: **0** matches. The skill uses "ready agent", "live agent", "psyche-wrapper" throughout; `$OWL listen` is never referenced (no allowlist exception needed).

**4. Manual smoke — exercised against a real gh-installed account (NOT only mocked):**
This machine has `gh` v2.89.0 authenticated as account `SaberMage`. Exercised against the real gh (trampoline bypassed via `SPT_TRAMPOLINE_GUARD=1`):
- `psyche-sync-setup` with `gh` removed from PATH → prints `gh CLI not found...`, exits **2**. ✓
- `sync-pull-async --agent doyle` on fresh `state=Unset` → silent, exits **0** (no git subprocess). ✓
- `psyche-sync-setup` with seeded `state=Enabled` (real gh present + authed) → idempotent `sync already enabled; remote=... acked=...`, exits **0**, no repo created. ✓
- `psyche-sync-setup --disable` → `sync disabled...`, exits **0**, settings.json flips to `state=failing` + `last_failure_reason=user-disabled`. ✓

The full happy-path `accept_flow` (actual `gh repo create`) was NOT run against the live account — that mutates the user's real GitHub account (creates a public-facing repo) and is out of an executor's autonomous remit. The happy path is covered by the Unix-gated fake-gh tests (`accept_flow_happy_persists_enabled`, `setup_git_invoked_once`, `push_all_from_seed`), which on this Windows host are compile-skipped; they will run under Unix CI.

## Verification

- `cargo build --release` — clean (warnings are pre-existing, unrelated modules).
- `cargo test --lib common::sync:: owl::psyche_sync_setup::` single-threaded — **20/20 pass**.
- `grep SyncPullAsync|PsycheSyncSetup src/cli.rs` → 2 enum variants.
- `grep Commands::SyncPullAsync|Commands::PsycheSyncSetup src/owl/mod.rs` → 2 dispatch arms.
- `grep 'pub fn run' src/owl/psyche_sync_setup.rs` → 1.
- `plugin/spt/skills/psyche-sync-setup/SKILL.md` exists; vocabulary gate = 0.
- Smoke: gh-absent exit 2; fresh `sync-pull-async` exit 0; idempotent exit 0; `--disable` exit 0.

## Deviations from Plan

None — plan executed as written. Action step E (update `plugin.json` skill enumeration) was verified to be a no-op: `plugin.json` carries no `skills` array (skills are auto-discovered by directory), so nothing needed adding.

## Deferred Issues

**Full-suite parallel-test failures (pre-existing, NOT introduced by this plan):** Running the entire `cargo test --lib` suite in parallel produces ~53 failures across unrelated modules (`listener`, `owlery`, `spool`, `tracked`, `context`, `fork`, `signoff`, `wrapper`, `doctor`, `hook_idle`). These are SPT_HOME-global state collisions between tests lacking `#[serial]` — already logged in PROJECT.md backlog ("21 unrelated parallel-test failures in Phase 18/28/30/32 surfaces; pass in isolation; cleanup quick task candidate"). All 20 of this plan's tests — plus the two co-listed pre-existing tests (`maybe_add_origin_adds_remote_when_enabled`, `sync_settings_round_trip_file`) — pass cleanly single-threaded. Out of scope per the executor SCOPE BOUNDARY (pre-existing failures in unrelated files); not fixed here.

## Known Stubs

None. `accept_flow` and the dispatcher are fully wired to real `gh`/`git` subprocesses and the live settings.json data layer. The SKILL.md is the runtime front door consumed by claude-code.

## Self-Check: PASSED

- Created files exist: `src/owl/psyche_sync_setup.rs`, `plugin/spt/skills/psyche-sync-setup/SKILL.md`, `35-04-SUMMARY.md`.
- Commits exist: `8dc1ad3` (Task 1), `5f8ffed` (Task 2).
