---
status: not-started
phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo
source: [35-09-PLAN.md]
kind: operator-driven
ci: false
operator: <name>
date_started: <iso8601>
date_completed: <iso8601>
---

# Phase 35 Operator UAT — Two-Machine Real-GitHub Sync

**Status**: Not Started | In Progress | Passed | Failed
**Operator**: <name>
**Date Started**: <iso8601>
**Date Completed**: <iso8601>

> This UAT exercises the parts CI cannot: real `gh` auth, the real GitHub API,
> real two-machine cross-machine context propagation, and real failure-injection
> recovery. The four `cargo test --test sync_*` integration tests (Plan 35-09)
> already prove the in-tree wiring against fake remotes; this runbook is the
> milestone-close gate that proves it against production GitHub.
>
> **Operator-driven, NOT part of CI.** No external verifier — this is an
> operator-trusted UAT (T-35-09-04). Record actual outputs (sanitized of any
> token / literal username) in a companion `35-OPERATOR-UAT-RUN.md` at
> completion. Phase 35 is closeable only when Sections 1–5 PASS.

## Prerequisites

- Two machines (M_A and M_B), preferably different OSes (one Windows, one Unix)
  for cross-platform validation.
- A GitHub account with NO existing `spt-agent-storage` repo.
- `gh` CLI 2.40+ installed on both, authenticated with the `repo` scope
  (`gh auth status` shows `repo`).
- SPT plugin (post-Phase 35, v1.8 milestone build) installed on both via
  `docs/DEPLOY.ps1`. NOTE: Plan 35-09 does NOT bump the plugin version — the
  single v1.8 milestone bump happens at milestone close, so confirm the build
  under test is the milestone-close binary, not a per-phase bump.
- `git` 2.30+ on PATH on both machines.

## SC Coverage Map

| SC | Description | Covered by |
|----|-------------|-----------|
| SC1 | Auto-detect path (D-08 SessionStart prompt) | Section 2 |
| SC2 | Manual setup path (`/psyche-sync-setup`) | Section 1 |
| SC3 | settings.json `sync.state` flag persistence | Sections 1, 4 |
| SC4 | Every Psyche-context commit triggers a sync cycle | Section 3 |
| SC5 | UserPromptSubmit async pull on the receiving machine | Section 3 |
| SC6 | Graceful no-op when unconfigured / backoff | Section 5 (5b, 5c) |
| SC7 | `owl doctor` surfaces sync status per machine | Sections 4, 5 |

## Section 1 — Manual Setup on M_A (SC2, SC3)

1. On M_A, run `$LIVE start doyle`. The sync-setup offer is delivered as an
   **active owl message** to doyle's own perch (carrying the
   `<spt-psyche-sync-prompt>` envelope with a self-contained `<instructions>`
   element). It surfaces **in-session** at the next tool-call boundary / poll —
   it is NOT deferred to the next boot. Confirm the live agent renders a
   three-option `AskUserQuestion` (`Enable now` / `No, never` / `Remind me in 12h`).
2. Choose "Enable now" → SPT invokes `/psyche-sync-setup`.
3. The skill runs `gh --version` (PASS), `gh auth status` (PASS), then
   `$OWL psyche-sync-setup`.
4. owl creates the private repo `{user}/spt-agent-storage` on GitHub.
5. Verify via gh: `gh repo view {user}/spt-agent-storage` shows the repo and
   reports it as **private**.
6. Verify settings.json: `$SPT_HOME/settings.json` → `sync.state == "enabled"`
   and `sync.remote_url == "https://github.com/{user}/spt-agent-storage.git"`.

   - **expected**: repo exists + private; `sync.state == "enabled"`; `remote_url`
     is the canonical HTTPS form; `acked_ts` is stamped.
   - **result**: <pass | fail>
   - **evidence**: <sanitized gh output + settings.json sync block>

## Section 2 — Auto-Detect on M_B (SC1)

1. On M_B (fresh install), run `$LIVE start doyle`. The repo already exists
   (created on M_A), but the prompt fires regardless (per Pitfall 9 — the
   prompt is NOT gated on repo-absence). The offer is delivered as an **active
   owl message** to doyle's perch and renders **in-session** at the next
   tool-call boundary / poll (NOT deferred to next boot). The same active
   delivery also fires on a `/clear`|`/compact` boundary for a live agent.
2. The live agent renders the same three-option `AskUserQuestion` from the
   delivered `<spt-psyche-sync-prompt>` envelope's `<instructions>` element.
3. Choose "Enable now" → `/psyche-sync-setup`.
4. `accept_flow` runs `gh repo create`; GitHub returns "already exists" → the
   idempotent path adds the remote + pulls. Verify M_B's local agent worktree
   (`$SPT_HOME/psyches/tracked/agents/doyle/`) contains whatever content M_A
   pushed (at minimum the seed bootstrap commit).
5. **Alternative path** (if `accept_flow` does NOT gracefully handle the existing
   remote): the operator manually runs
   `git -C {agent-worktree} remote add origin {url}` then
   `git -C {agent-worktree} pull origin a-doyle`. **Document the gap** in the
   RUN companion file if this path is hit — it is a defect to file, not a PASS.

   - **expected**: re-run is idempotent; M_B worktree carries M_A's content;
     `sync.state == "enabled"` on M_B.
   - **result**: <pass | fail>
   - **evidence**: <sanitized>

## Section 3 — Trigger Verification: M_A → M_B propagation (SC4, SC5)

1. On M_A: send a commune to doyle (e.g. via `/spt:commune` or a file-drop).
   Confirm the commit landed in the agent worktree's `a-doyle` branch
   (`git -C {agent-worktree} log -1 --oneline`).
2. Confirm the push happened:
   `gh api repos/{user}/spt-agent-storage/commits?sha=a-doyle` shows the new
   commit (SC4 — post-commit sync fired).
3. On M_B: open a new claude-code session for doyle (`$LIVE start doyle` again,
   or simply submit any prompt). The UserPromptSubmit hook fires the detached
   async pull (SC5).
4. Within ~30s, M_B's agent worktree contains the M_A commune content.
   `$LIVE psyche-download doyle` shows the new content.

   - **expected**: commit visible on `a-doyle` via gh API after M_A commune;
     M_B worktree gains the content within one prompt cycle.
   - **result**: <pass | fail>
   - **evidence**: <sanitized gh api output + M_B worktree diff>

## Section 4 — Doctor Verification (SC3, SC7)

1. On BOTH machines: run `$OWL doctor`. Confirm the `## Sync` section shows:
   - Global row: `sync   PASS   enabled; remote=...`
   - Per-branch row: collapsed `sync:branches   PASS   all branches clean`
   - No recovered-aborts row (recovered_aborts == 0 on a healthy machine).

   - **expected**: both machines show `sync PASS enabled` with the correct
     remote; no FAIL/WARN rows.
   - **result**: <pass | fail>
   - **evidence**: <sanitized doctor `## Sync` section, both machines>

## Section 5 — Failure Injection (SC6, SC7)

### 5a — 404 (repo deleted) → Failing hard stop (D-17)

1. On GitHub Web, delete `spt-agent-storage`.
2. On M_A: send another commune. The post-commit push hits a 404.
3. `$OWL doctor` on M_A shows:
   `sync   FAIL   FAILING (remote-404 — repo gone or never created); run /psyche-sync-setup to re-create remote`.
4. Run `/psyche-sync-setup`; re-create the repo; verify `sync.state` transitions
   back to `enabled`.

   - **expected**: 404 flips state to `failing`; doctor surfaces the remediation;
     re-running setup recovers to `enabled`.
   - **result**: <pass | fail>
   - **evidence**: <sanitized>

### 5b — Backoff progression (D-18) → graceful no-op (SC6)

1. On M_B, disconnect the network. Submit a prompt → the UserPromptSubmit hook
   fires; the pull fails (network). Doctor shows `last-err` + `retry-after`.
2. Wait until `retry-after` elapses; the next prompt triggers a retry. After
   several rapid failures, observe the schedule progression
   (60s → 300s → 900s → ...).
3. Confirm the sync failure NEVER blocks the prompt / agent — communes and
   normal work continue (SC6 graceful no-op posture).

   - **expected**: failures recorded with escalating `retry-after`; agent work
     never blocked; reconnecting + waiting out the gate resumes sync.
   - **result**: <pass | fail>
   - **evidence**: <sanitized doctor over time>

### 5c — gh auth revoke → plain git failure under backoff (D-16, SC6)

1. Run `gh auth logout`. The next sync manifests as a plain git auth failure
   (per D-16: NO special auth-failing state — it routes through the transient
   backoff path like any other failure).
2. Doctor shows the failure under transient backoff (`last-err` + `retry-after`),
   NOT a distinct auth state.
3. Run `gh auth login` to recover. Sync resumes on the next attempt WITHOUT
   re-running `/psyche-sync-setup`.

   - **expected**: auth failure → transient backoff (not a special state);
     `gh auth login` alone restores sync.
   - **result**: <pass | fail>
   - **evidence**: <sanitized>

## Acceptance

Sections 1–5 PASS. The operator records actual outputs (sanitized of tokens and
the literal GitHub username) in a companion `35-OPERATOR-UAT-RUN.md` file next to
this runbook at completion. **Phase 35 is closeable only when this UAT passes.**

If ANY section fails:
1. Capture the failure mode + sanitized evidence in `35-OPERATOR-UAT-RUN.md`.
2. Create a debug todo in `.planning/debug/` describing the gap.
3. Do NOT mark Phase 35 closable — return to the planner with the gap.

## Cleanup (T-35-09-02)

After the UAT completes, the operator MAY delete `spt-agent-storage` from GitHub
to avoid leaving a real repo behind (Section 5a already exercises deletion). The
RUN companion file, if it contains the literal username, should be kept
operator-private and NOT committed (T-35-09-01).
