---
phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo
plan: 05
subsystem: infra
tags: [sync, hook, userpromptsubmit, detached-spawn, gh, cross-machine, git]

# Dependency graph
requires:
  - phase: 35-02
    provides: "src/common/sync.rs helpers — is_backoff_active, gh_present"
  - phase: 35-03
    provides: "src/common/process.rs::spawn_detached_unix (Unix detached spawn + Windows stub)"
provides:
  - "UserPromptSubmit hook fires a fire-and-forget detached owl.exe sync-pull-async after every prompt"
  - "private fn dispatch_async_sync_pull(self_id) in src/owl/hook_prompt.rs"
  - "tests/source_order_hook_prompt.rs — 3 source-order pins protecting the orthogonal-hook posture"
affects: [35-06, 35-07, 35-08, 35-09]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Orthogonal-hook side-effect dispatch appended AFTER all existing branches (Phase 25.4 / hook_idle.rs:62-82 precedent)"
    - "cfg-split detached spawn: win_spawn::spawn_detached_no_inherit (Windows) / process::spawn_detached_unix (Unix)"
    - "include_str! byte-offset source-order test pins (Phase 28/29 precedent)"

key-files:
  created:
    - tests/source_order_hook_prompt.rs
  modified:
    - src/owl/hook_prompt.rs

key-decisions:
  - "Self-id variable at dispatch scope is owl_id (resolved at hook_prompt.rs:36-39); passed by reference as self_id"
  - "No-early-return Test 3 kept STRICT-ZERO form — run() had zero `return;` (semicolon) statements pre-edit; its no-perch guard uses a match arm `None => return,` (comma) which does not match `return;`"
  - "Detached child env passed empty on Windows (&[]); Unix mirror inherits parent env (own binary, not untrusted)"

patterns-established:
  - "Pattern: fire-and-forget side-effect dispatcher lives as the LAST statement in a hook run() fn, after all output_hook_response calls, gated internally with its own early-returns (which are safe — orthogonal-hook rule applies to run()'s flow, not the helper's)"

requirements-completed: [SYNC-HOOK-01]

# Metrics
duration: 7min
completed: 2026-05-26
---

# Phase 35 Plan 05: UserPromptSubmit Async-Pull Dispatch Summary

**Every UserPromptSubmit hook now fires a fire-and-forget detached `owl.exe sync-pull-async`, appended after both existing branches' output_hook_response calls, gated on `sync.state == Enabled` AND no active D-18 backoff.**

## Performance

- **Duration:** ~7 min
- **Started:** 2026-05-26T23:52:24Z (approx, continuing from STATE last_updated)
- **Completed:** 2026-05-26
- **Tasks:** 1
- **Files modified:** 2 (1 modified, 1 created)

## Accomplishments

- Added private `dispatch_async_sync_pull(self_id: &str)` helper to `src/owl/hook_prompt.rs`.
- Call site appended at the END of `pub fn run()`, AFTER both the wake-sentinel branch and the non-wake branch have completed their `output_hook_response` calls — the orthogonal-hook side-effects principle (Phase 25.4 / hook_idle.rs:62-82) is preserved.
- Dispatcher silently no-ops when `sync.state != Enabled`, when `is_backoff_active` is true (D-18 gate), or when `current_exe()` resolution fails.
- cfg-split detached spawn: `win_spawn::spawn_detached_no_inherit(&exe, &args_ref, &[])` on Windows, `process::spawn_detached_unix(&exe, &args_ref)` on Unix. Child PID return is ignored (fire-and-forget).
- Created `tests/source_order_hook_prompt.rs` with 3 source-order pins.

## Task Commits

Each task was committed atomically:

1. **Task 1: dispatch_async_sync_pull helper + call site appended after both branches** - `e626373` (feat)

**Plan metadata:** _(this commit)_ (docs: complete plan)

## Files Created/Modified

- `src/owl/hook_prompt.rs` - Added `dispatch_async_sync_pull` helper (line 146) and its call site `dispatch_async_sync_pull(&owl_id);` (line 129) at the end of `run()`.
- `tests/source_order_hook_prompt.rs` - 3 tests: `dispatcher_runs_after_both_branches`, `dispatcher_called_exactly_once`, `no_early_return_short_circuits_dispatcher`.

## Plan Output Questions Answered

- **Insertion-point:** Call site is `src/owl/hook_prompt.rs:129`, immediately after the non-wake branch's closing `}` and before `run()`'s final `}`. The helper definition begins at line 146.
- **Self-id variable name at scope:** `owl_id` (a `String`, resolved at lines 36-39). Passed as `&owl_id` to the helper's `self_id: &str` parameter.
- **Pre-existing `return;` refactoring:** NONE required. `run()` contained zero `return;` (semicolon) statements before this change. Its only early exits are match arms (`None => return,` with a comma) which do not match the `return;` token. No branch needed to be converted to fall-through.
- **No-early-return test final form:** STRICT-ZERO. `no_early_return_short_circuits_dispatcher` asserts the comment-stripped slice from `pub fn run` up to the dispatcher call contains zero `return;` occurrences. Confirmed valid by the read_first analysis (grep for `return;` in hook_prompt.rs returned no matches pre-edit).

## Decisions Made

- Kept Test 3 in its strict-zero form rather than downgrading to baseline-comparison, because the pre-existing `run()` had no `return;` statements to baseline against.
- Windows spawn passes an empty env slice (`&[]`) so the child inherits the parent environment, matching the Unix mirror's inherit-env posture (the dispatched binary is our own `owl.exe`, not untrusted — per process.rs Phase 35 doc-comment).

## Deviations from Plan

None - plan executed exactly as written.

## Issues Encountered

None. The plan's interface contracts (sync.rs helpers, owlery readers, win_spawn/process spawn signatures, cli SyncPullAsync subcommand) all matched the code in tree. `now_iso_utc` is `pub(crate)` in `src/common/time.rs` and resolves correctly via `crate::common::time::now_iso_utc()`.

## Verification Results

- `cargo build --release` — clean (only pre-existing unrelated dead-code warnings).
- `cargo test --test source_order_hook_prompt` — 3 passed, 0 failed.
- `cargo test --lib hook_prompt` — 0 matched (no pre-existing hook_prompt lib unit tests; 931 filtered out, no regression surface).
- `grep 'fn dispatch_async_sync_pull\|dispatch_async_sync_pull('` → exactly 2 lines (definition + call).
- `grep 'spawn_detached_no_inherit\|spawn_detached_unix'` → exactly 2 lines (one per cfg arm).

## Threat Surface

No new security-relevant surface introduced beyond the plan's `<threat_model>`. The dispatcher passes the pre-validated `owl_id` via `--agent` argv (not shell-interpolated), spawns a non-inheriting detached child (no stdio leakage), and uses `CREATE_BREAKAWAY_FROM_JOB` on Windows. All threat-register `mitigate` dispositions (T-35-05-02/03/04/05) are satisfied by the implementation as written.

## Next Phase Readiness

- The first D-04 async-pull trigger is wired. Plan 35-06 (`maybe_add_origin` worktree wiring), 35-07 (boot-path gh probe), 35-08 (doctor surface), and 35-09 (integration tests including the rapid-prompt T-35-05-01 scenario) can proceed.
- No blockers.

## Self-Check: PASSED

- FOUND: src/owl/hook_prompt.rs (modified — dispatch_async_sync_pull present at lines 129 + 146)
- FOUND: tests/source_order_hook_prompt.rs (created)
- FOUND: commit e626373

---
*Phase: 35-psyche-sync-cross-machine-context-backup-via-private-gh-repo*
*Completed: 2026-05-26*
