---
phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo
plan: 10
verified: 2026-05-27T00:00:00Z
status: human_needed
score: 5/5 must-haves verified (automated scope)
overrides_applied: 0
human_verification:
  - test: "On `$LIVE start <id>`, observe that the AskUserQuestion (Enable now / No, never / Remind me in 12h) actually renders to the user in-session — not deferred to a later boot."
    expected: "Within the same `$LIVE start` session, at the next tool-call boundary or poll, the live agent renders an AskUserQuestion with exactly the three option labels. Selecting 'Enable now' invokes /psyche-sync-setup. Selecting 'No, never' marks sync declined (no re-prompt). Selecting 'Remind me in 12h' defers with a 12h cooldown."
    why_human: "The render-path test proves the spool row lands with the correct envelope body. The AskUserQuestion UI hop (spool drain -> PreToolUse hook -> <owl_messages> injection -> Claude renders AskUserQuestion) cannot be asserted by cargo test — it requires a live Claude Code session with a running live agent."
  - test: "On a `/clear` or `/compact` boundary for a live agent, the same AskUserQuestion renders in-session."
    expected: "After /clear or /compact with a live agent perch present (sync state Unset or RemindLater-past-cooldown), the offer appears in-session before the next user interaction completes."
    why_human: "Same reason as above — the clear/compact trigger is unit-tested (deliver_sync_prompt_to_perch call site in inject_reorientation_if_needed is wired), but the end-to-end in-session render requires a live Claude Code session."
  - test: "Full two-machine real-GitHub UAT (35-OPERATOR-UAT.md Sections 1-5)."
    expected: "All five sections PASS per the runbook: SC1 auto-detect (Section 2), SC2 manual setup (Section 1), SC3 settings persistence, SC4 post-commit sync trigger, SC5 UserPromptSubmit async pull, SC6 graceful no-op, SC7 doctor surfaces sync status."
    why_human: "End-to-end cross-machine sync requires two real machines, real gh CLI auth, and the real GitHub API. The four cargo test --test sync_* integration tests exercise wiring against fake remotes; operator UAT (35-OPERATOR-UAT.md) is the milestone-close gate for real-GitHub validation. Phase 35 is closeable only when that UAT passes."
---

# Phase 35 Plan 10: Active Sync-Prompt Delivery (SC1 Gap-Closure) Verification Report

**Phase Goal:** Fix the Phase 35 SC1 defect: the cross-machine sync auto-detect prompt (`<spt-psyche-sync-prompt>`) never renders. Replace the passive-emit path with active delivery mirroring the version-change pattern.
**Plan 10 Focus:** gap-closure plan for SYNC-AUTO-01 — active owl-message delivery of the sync-setup AskUserQuestion.
**Verified:** 2026-05-27
**Status:** human_needed
**Re-verification:** No — initial verification.

## Goal Achievement

### Observable Truths (from 35-10-PLAN.md must_haves.truths)

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | On `$LIVE start`/`$LIVE revive` for a live agent, the sync-setup AskUserQuestion renders via the live agent's active channel — NOT passive SessionStart additionalContext. | VERIFIED | `src/live/start.rs:401` and `:728` both call `crate::owl::sync_prompt::deliver_sync_prompt_to_perch(id)`. The old `queue_sync_prompt_if_due()` is gone (grep confirms zero hits). `deliver_sync_prompt_to_perch` routes via `send::deliver_body` (TCP-first, spool fallback). |
| 2 | On a `/clear` or `/compact` SessionStart boundary where a live agent perch is present, the same AskUserQuestion renders via active owl-message delivery. | VERIFIED | `src/owl/plugin_session_start.rs:366`: `deliver_sync_prompt_to_perch(&id)` called inside the `if is_live` guard within `inject_reorientation_if_needed`. Gated strictly on `is_live` (line 354). Non-live and startup paths never reach this call. |
| 3 | The delivered envelope carries a self-contained `<instructions>` element instructing Claude to render the three-option AskUserQuestion — Claude does not need any prior skill load to act on it. | VERIFIED | `src/owl/sync_prompt.rs:108-111`: `build_sync_prompt_block()` formats a static literal `<spt-psyche-sync-prompt>\n  <instructions>\n{instructions}\n  </instructions>\n</spt-psyche-sync-prompt>`. Unit test `block_contains_self_contained_instructions_element` asserts nesting and ordering. Tests pass (11/11). |
| 4 | Non-live and plain-startup-only paths no longer emit the sync prompt (live-agent-users-only scope per locked decision 1). | VERIFIED | `plugin_session_start.rs:194-226`: the old Step 6.6 sentinel-consume + `emit_sync_prompt()` block is deleted (replaced by a comment at line 218-225). `inject_reorientation_if_needed` returns early for non-clear/compact sources (line 330); the `is_live` guard at line 354 prevents delivery for non-live perches. grep for `build_sync_prompt_envelope\|fn emit_sync_prompt\|sync-prompt-due\|queue_sync_prompt_if_due` in `src/` returns zero function definitions (only tombstone comments). |
| 5 | `$OWL doctor` renders enabled-but-failing / backoff per-agent sync rows as WARN/FAIL, not green PASS. | VERIFIED | `src/owl/doctor.rs:972-976`: status is `Fail` when `backoff_active || hard_stop`, `Warn` when failures > 0 but no active backoff, `Pass` only when `any_failure == false`. Three new regression tests pass: `check_sync_status_enabled_failing_branch_expands` (active backoff → Fail), `check_sync_status_enabled_failing_elapsed_backoff_is_not_pass` (elapsed backoff → Warn not Pass), `check_sync_status_enabled_hard_stop_reason_is_fail` (remote-404 → Fail). `cargo test --lib owl::doctor`: 22/22 passed. |

**Score: 5/5 truths verified (automated scope)**

### Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/owl/sync_prompt.rs` | `build_sync_prompt_block()` + `deliver_sync_prompt_to_perch()` + relocated predicate; contains `spt-psyche-sync-prompt` | VERIFIED | File exists, 301 lines. All three exports present and substantive. `build_sync_prompt_block()` is a static-literal builder. `deliver_sync_prompt_to_perch()` reads settings, probes gh, gates, delivers via `send::deliver_body`, bumps `last_prompted_ts`. 11 unit tests cover block shape and predicate truth table. |
| `tests/sync_prompt_render.rs` | Render-path test exercising deliver -> spool-landing + `<instructions>` presence | VERIFIED | File exists, 238 lines. 3 test cases: Unset+gh → one spool row with `<spt-psyche-sync-prompt>` and `<instructions>` in body; Declined → 0 rows; Enabled → 0 rows. Queries `spool.db` directly (delivery mechanism assertion). `cargo test --test sync_prompt_render`: 3/3 passed. |
| `plugin/spt/skills/live/SKILL.md` | Always-loaded `<spt-psyche-sync-prompt>` handler section | VERIFIED | Lines 251-262: "Handling the `<spt-psyche-sync-prompt>` envelope" section present. States the `<instructions>` element is authoritative; lists all three option labels verbatim with their actions. Framed as belt-and-suspenders. `grep -c "spt-psyche-sync-prompt" plugin/spt/skills/live/SKILL.md` returns 2 (section heading + body). |

### Key Link Verification

| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `src/live/start.rs` | `src/owl/sync_prompt.rs::deliver_sync_prompt_to_perch` | `$LIVE start` call site (line 401) | WIRED | `let _ = crate::owl::sync_prompt::deliver_sync_prompt_to_perch(id);` at line 401, after perch setup, before wrapper spawn. |
| `src/live/start.rs` | `src/owl/sync_prompt.rs::deliver_sync_prompt_to_perch` | `$LIVE revive` call site (line 728) | WIRED | `let _ = crate::owl::sync_prompt::deliver_sync_prompt_to_perch(id);` at line 728, same placement as start path. |
| `src/owl/plugin_session_start.rs` | `src/owl/sync_prompt.rs::deliver_sync_prompt_to_perch` | clear/compact `is_live` branch of `inject_reorientation_if_needed` (line 366) | WIRED | Inside `if is_live { ... deliver_sync_prompt_to_perch(&id) }` at line 366. Strictly gated on `is_live` (line 354). |
| `src/owl/sync_prompt.rs` | `crate::owl::send::deliver_body` | active owl-message delivery to live perch | WIRED | Line 145: `crate::owl::send::deliver_body(target_owl_id, "psyche-sync-prompt", &body);`. TCP-first, spool fallback. Render-path test confirms spool row lands when TCP listener absent. |

**Three production call sites confirmed:** `deliver_sync_prompt_to_perch` is called from exactly three locations matching the plan acceptance criteria.

### Data-Flow Trace (Level 4)

| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `deliver_sync_prompt_to_perch` | `body` from `build_sync_prompt_block()` | Static literal — no dynamic data needed | Yes (static, by design — T-35-10-01) | VERIFIED |
| `deliver_sync_prompt_to_perch` | `settings` (predicate gate) | `read_sync_settings()` reads `$SPT_HOME/settings.json` | Yes | VERIFIED |
| `should_emit_sync_prompt` | `gh_present` | `crate::common::sync::gh_present()` subprocess probe | Yes | VERIFIED |

Note: the body is intentionally a static literal (T-35-10-01 input-validation parity with version-change T-35-07). No dynamic user data interpolated — this is the correct design.

### Behavioral Spot-Checks

| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| `cargo test --lib owl::sync_prompt` (11 unit tests) | `cargo test --lib owl::sync_prompt -- --test-threads=1` | 11 passed, 0 failed | PASS |
| `cargo test --lib owl::doctor` (22 unit tests including 3 new regression tests) | `cargo test --lib owl::doctor` | 22 passed, 0 failed | PASS |
| `cargo test --test sync_prompt_render` (3 render-path tests) | `cargo test --test sync_prompt_render` | 3 passed, 0 failed | PASS |

### Probe Execution

No conventional probe scripts declared or present for plan 35-10. The plan's verification criteria use `cargo test` invocations (run above).

### Requirements Coverage

| Requirement ID | Source Plan(s) | Status | Evidence |
|----------------|----------------|--------|----------|
| SYNC-BOOTSTRAP-01 | 35-01, 35-04, 35-09 | SATISFIED (prior plans; not 35-10 scope) | Plans 01/04/09 cover bootstrap — prior plans; not retested here. |
| SYNC-AUTO-01 | 35-07, 35-09, **35-10** | SATISFIED | 35-10 is the gap-closure plan for this requirement. All 5 must-have truths verified. Active delivery path wired at 3 call sites. Tests pass. |
| SYNC-MANUAL-01 | 35-04, 35-09 | SATISFIED (prior plans; not 35-10 scope) | Manual setup via `/psyche-sync-setup` — prior plans; not retested here. |
| SYNC-HOOK-01 | 35-02, 35-03, 35-05, 35-06 | SATISFIED (prior plans; not 35-10 scope) | Post-commit + UserPromptSubmit hooks — prior plans; not retested here. |
| SYNC-NOOP-01 | 35-02, 35-08 | SATISFIED (prior plans; not 35-10 scope) | Graceful no-op / doctor surface — prior plans; not retested here. |

All five declared requirement IDs are accounted for across plans 01-10. No orphaned requirements found (REQUIREMENTS.md does not exist in `.planning/` — requirements are tracked inline in ROADMAP.md and PLAN frontmatter for this project).

### Passive-Emitter Removal Verification

| Check | Expected | Result |
|-------|----------|--------|
| `grep -rn "build_sync_prompt_envelope\|fn emit_sync_prompt" src/` | Zero function definitions | PASS — only tombstone comments in `plugin_session_start.rs` (lines 158, 1275); no callable function definitions remain |
| `grep -rn "sync-prompt-due\|queue_sync_prompt_if_due" src/` | Zero live code | PASS — only tombstone comments in `sync.rs` (lines 197, 1183-1185) and `start.rs` (line 392 comment); no sentinel writes or reads remain |
| `src/owl/mod.rs:25` | `pub mod sync_prompt;` declared | PASS — confirmed at line 25 |

### Prior Plans Untouched Check

Commits `286fe40`, `e8c78ee`, `2c17cd9`, `d0fcfb6` (the four 35-10 commits) touch exactly these files:

- Created: `src/owl/sync_prompt.rs`, `tests/sync_prompt_render.rs`, `deferred-items.md`
- Modified: `src/owl/mod.rs`, `src/owl/plugin_session_start.rs`, `src/live/start.rs`, `src/common/sync.rs`, `src/owl/doctor.rs`, `plugin/spt/skills/live/SKILL.md`, `35-OPERATOR-UAT.md`

No prior-plan source files (`35-01` through `35-09` scope files) appear in these commit diffs. The nine prior plans are untouched.

### Anti-Patterns Found

| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/owl/plugin_session_start.rs` | 1275 | Tombstone comment referencing deleted `build_sync_prompt_envelope` | Info | Documentation only — no callable code. Intentional audit trail of the deletion. Not a live stub. |
| `src/common/sync.rs` | 197, 1183-1185 | Tombstone comments referencing deleted `queue_sync_prompt_if_due` | Info | Documentation only — same as above. |

No TBD/FIXME/XXX markers introduced by 35-10 commits. No stub implementations. No hardcoded empty returns on render paths.

**Pre-existing known issue (NOT attributed to 35-10):** `tests/native_wrapper_state_retry.rs` fails to compile (`missing field pulse_psyche`) — last touched by commit 844d903 (phase 25.2). Confirmed absent from all 35-10 commits. Logged in `deferred-items.md`. This causes bare `cargo test` (full suite) to fail to compile; targeted test invocations (used above) are unaffected.

### Human Verification Required

#### 1. In-Session AskUserQuestion Render on `$LIVE start`

**Test:** With sync state set to `Unset` and `gh` CLI present, run `$LIVE start <id>` in a live Claude Code session. Observe the session for an `AskUserQuestion` popup offering cross-machine Psyche context sync.
**Expected:** At the next tool-call boundary or poll after start, the live agent renders an `AskUserQuestion` with exactly three options: "Enable now", "No, never", "Remind me in 12h". Each option performs its documented action.
**Why human:** The render-path test (`sync_prompt_render.rs`) proves the spool row lands with the correct body. The actual AskUserQuestion UI hop (PreToolUse hook drains spool as `<owl_messages>` XML, Claude reads it, Claude invokes AskUserQuestion) requires a live Claude Code session. The final hop is a model behavior, not an automatable assertion.

#### 2. In-Session AskUserQuestion Render on `/clear` or `/compact` Boundary

**Test:** With a live agent running and sync state `Unset`, run `/clear` or `/compact` in the Claude Code session. Observe the session for an `AskUserQuestion` popup.
**Expected:** After the clear/compact, on the next tool-call boundary, the same three-option AskUserQuestion renders in-session (not deferred to a later boot).
**Why human:** Same as item 1 — the clear/compact delivery path is wired and unit-tested, but the AskUserQuestion render requires live observation.

#### 3. Full Two-Machine Operator UAT (35-OPERATOR-UAT.md Sections 1–5)

**Test:** Complete all five sections of `35-OPERATOR-UAT.md` against a real GitHub account on two machines.
**Expected:** Sections 1 (manual setup, SC2), 2 (auto-detect, SC1), 3 (trigger verification M_A → M_B propagation, SC4/SC5), 4 (doctor verification, SC3/SC7), and 5 (failure injection, SC6/SC7) all PASS. Results recorded in a companion `35-OPERATOR-UAT-RUN.md`.
**Why human:** Real `gh` auth, real GitHub API, real two-machine cross-machine context propagation, and real failure-injection recovery cannot be exercised in CI or automated tests. This UAT is the milestone-close gate for Phase 35 per the runbook: "Phase 35 is closeable only when Sections 1–5 PASS."

---

## Gaps Summary

No automated-scope gaps found. All five must-have truths are VERIFIED, all three artifacts exist and are substantive and wired, all three key links are connected, all targeted test suites pass, and the passive emitter is fully removed.

The `human_needed` status reflects three items that require live Claude Code sessions or a real two-machine GitHub setup to validate — specifically the AskUserQuestion in-session render (SC1/SC2 end-to-end) and the full two-machine operator UAT. These are not defects; they are the correct verification boundary for behaviors that cannot be asserted by `cargo test`.

---

_Verified: 2026-05-27_
_Verifier: Claude (gsd-verifier)_
