---
phase: 35.3-psyche-sync-setup-ux-pass-error-display-doctor-partial-docs
plan: 03
subsystem: owl-doctor
tags: [doctor, sync, partial-setup, diagnostics, git-probe]
requires:
  - "35.3-01 (GitError Display lock + run_git_with_timeout present in git.rs)"
provides:
  - "doctor sync:partial Warn row for half-succeeded sync-setup (state==Unset + origin configured)"
  - "git::run_git_with_timeout promoted to pub(crate) — reusable bounded git runner"
affects:
  - "src/owl/doctor.rs check_sync_status (Unset branch only)"
  - "src/common/git.rs run_git_with_timeout visibility"
tech-stack:
  added: []
  patterns:
    - "Reuse the 500ms-bounded run_git_with_timeout for diagnostic git probes (no hand-rolled Command)"
    - "Substring origin detection on the bare-seed git config (seed/config), .git/config fallback"
key-files:
  created: []
  modified:
    - "src/common/git.rs"
    - "src/owl/doctor.rs"
decisions:
  - "Probe seed/config (bare-repo layout) in addition to seed/.git/config — the tracked seed is git init --bare, so origin config lives at seed/config; the plan's literal seed/.git/config alone would never fire"
  - "Warn-only on both probe arms (D-06); collapse rule preserved by appending inside the Unset branch before the state != Enabled short-circuit"
metrics:
  duration: ~9min
  completed: 2026-05-29
---

# Phase 35.3 Plan 03: Doctor Partial-Sync Probe Summary

Doctor now surfaces a `sync:partial` Warn row when a `/spt:psyche-sync-setup` half-succeeded — pushed an origin to the tracked seed but failed at the final settings.json write, leaving `state==Unset` — instead of the misleading "not configured" row. The probe reuses `git::run_git_with_timeout` for a 500ms-bounded `ls-remote origin` reachability check (promoted to `pub(crate)`), with a local-config substring check for origin presence.

## What Was Built

- **`src/common/git.rs`**: Promoted `run_git_with_timeout` from private `fn` to `pub(crate) fn` (D-04) so `owl::doctor` can reuse the timeout-bounded, console-flash-suppressed, zombie-safe git runner instead of hand-rolling a raw `Command::new("git")`. The arg-vector invocation carries the T-35.3-05 injection mitigation for free.
- **`src/owl/doctor.rs probe_partial_sync(seed_dir)`**: New private helper. Detects an `[remote "origin"]` section by substring-reading the seed repo's git config (`seed/config` for the bare layout, falling back to `seed/.git/config`), then runs a bounded `git ls-remote origin`. Returns:
  - `Some(Warn, D-07 wording)` — origin present AND `ls-remote` ok.
  - `Some(Warn, D-04 wording)` — origin present locally but `ls-remote` timed out/failed (remote unverified).
  - `None` — no origin (leave the plain "not configured" global row untouched).
- **`check_sync_status` wiring**: Inside the `SyncState::Unset` branch only, after the global (+recovered-aborts) rows and **before** the `state != Enabled` short-circuit, `out.push(probe_partial_sync(&owlery::seed_path()))` when `Some`. Non-Unset states are byte-identical to before. Collapse rule and exit semantics untouched (D-06).
- **Tests** (`#[cfg(test)] mod tests` in doctor.rs — module already existed, two cases added):
  - `probe_partial_sync_no_origin_returns_none` — seed config without origin → `None`. Env-independent (helper takes seed dir directly; no SPT_HOME/ENV_LOCK needed).
  - `probe_partial_sync_planted_origin_yields_warn_degrade` — planted `[remote "origin"]` at a fake path → `ls-remote` fails → D-04 degrade Warn row asserted (name `sync:partial`, status `Warn`, exact degrade wording). Git-presence gated.

## Verification

- `cargo build --release` — succeeds, clean (no `probe_partial_sync never used` warning after wiring).
- `cargo test --lib doctor::tests` — 24 passed, 0 failed (includes the two new probe tests).
- `cargo test --lib -- --test-threads=1` (full suite) — 962 passed, 0 failed, 5 ignored. No regression to other doctor rows or collapse behavior.
- D-07 verbatim string present in doctor.rs (grep: 1 occurrence).
- No raw `Command::new("git")` added in doctor.rs for the probe (reuse path only).

## Deviations from Plan

None affecting scope or behavior. One implementation clarification:

- **Probe target path:** The plan's `read_first`/behavior block named `seed/.git/config` as the origin-config probe target, while the `<interfaces>` note hedged ("the `.git/config` lives directly under it"). The tracked seed is created via `git init --bare seed/` (confirmed in `src/common/tracked.rs::ensure_seed`), so a bare repo's config lives at `seed/config`, not `seed/.git/config`. Probing only the literal `.git/config` path would have meant the row could never fire on the real seed. The helper checks both `seed/config` (bare — the real location) and `seed/.git/config` (non-bare fallback), faithful to the plan's intent of detecting an origin in the seed repo's git config. This is a within-scope path correction, not a behavior change.

## Threat Surface

No new trust boundaries beyond those in the plan's threat model. The `git ls-remote origin` invocation is arg-vector (no shell interpolation — T-35.3-05 mitigated) and 500ms-bounded with kill+reap (T-35.3-06 mitigated). Disposition Warn-only, exit semantics unchanged (T-35.3-07 accepted). No package installs (T-35.3-SC N/A).

## Known Stubs

None. The probe is fully wired and tested; both arms produce live Warn rows.

## Self-Check: PASSED

- src/common/git.rs — FOUND (modified, `pub(crate) fn run_git_with_timeout`)
- src/owl/doctor.rs — FOUND (modified, `probe_partial_sync` + wiring + tests)
- Commit 8387fb9 (Task 1) — FOUND
- Commit 0ae7b80 (Task 2) — FOUND
