# Phase 35.3: psyche-sync-setup-ux-pass-error-display-doctor-partial-docs - Research

**Researched:** 2026-05-29
**Domain:** Rust CLI (owl.exe) — error formatting, doctor diagnostics, SKILL.md docs, tracked-context sync coverage
**Confidence:** HIGH (all claims verified by direct source-trace in this session)

## Summary

Four of the five items in this phase are trivial and effectively locked: a one-character `{:?}`→`{}` flip (D-01), two SKILL.md doc edits (D-02, D-08), and a doctor Warn row with verbatim wording (D-03..D-07). These need only touchpoint confirmation — provided below — and a cheap regression test (D-12). The heavy lift is **Issue 7** (D-09..D-11), whose root cause was UNCONFIRMED at context-gathering time.

**Issue 7 root cause is CONFIRMED, and it is NOT what the user's symptom report suggested.** The user framed it as "gh detection false negative." That is a red herring: `gh_present()` and `should_emit_sync_prompt()` gate only the *prompt offer*, never tracked-context writing, and neither consults host-project git state. The actual mechanism is in **project-name resolution**: `project_name_from_cwd_path` (`src/common/owlery.rs:1039-1061`) enforces a STRICT git-ancestor-or-`None` contract introduced by Phase 25.3 (Defect B2). When a host project has no `.git` ancestor, the walk fails and the function returns `None` (step b, lines 1043-1050). Every tracked-context write site (`echo_commune.rs:498`, `594`, `931`) treats that `None` as `SkippedNoProjectName` and silently skips `ensure_project_worktree` + the file write + the commit. No worktree, no commit, nothing to sync. The agent slice still syncs (it keys off `self_id`, not cwd); only the *project* slice is starved.

**Primary recommendation:** Issue 7 is a **LOCALIZED fix that fits cleanly in 35.3** — add a guarded basename fallback to step (b) of `project_name_from_cwd_path`, gated by `validate_id_chars` and retaining the existing `tracked`/psyche_dir rejection. This mirrors the D-06 fallback that the sibling resolver `derive_current_repo_names` (`owlery.rs:982-985`) already has. No schema change, no new surface, ~10 lines + unit tests. The split-to-35.4 contingency is NOT triggered. For the four polish items: apply as specified in CONTEXT; they are mechanical.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **D-01:** One-character flip `{:?}` → `{}` at the `eprintln!` in `psyche_sync_setup.rs`. Both `SyncError` Display and nested `GitError` Display are verified clean. No Display-impl changes.
- **D-02:** Add a `**1**` bullet to the "Interpret the exit code" list in `plugin/spt/skills/psyche-sync-setup/SKILL.md` (currently 0/2/3/4/5). Phrasing: generic `accept_flow` failure; surface the human-readable error line; offer `$OWL doctor`.
- **D-03:** **Probe-only** doctor partial-state detection. In `doctor.rs::check_sync_status`, when `state == Unset`, probe `seed/.git/config` for `origin` AND `git ls-remote origin` success → surface a partial-setup row. **No SyncSettings schema change** (hard backward-compat constraint — shared users).
- **D-04:** The `ls-remote` probe MUST be timeout-bounded (reuse 35.2's timeout-wrapped git helper). On timeout/failure, degrade to "origin configured locally, remote unverified" rather than blocking/hanging. Doctor stays fast and non-fatal.
- **D-05:** `accept_flow_attempt_ts` persistence is **deferred** (see Deferred Ideas). Probe gives the signal now without schema churn.
- **D-06:** Severity = **Warn**, not Fail. Partial setup is recoverable; Fail stays reserved for `state == Failing`. Don't perturb doctor's overall exit semantics.
- **D-07:** Row message points at the idempotent re-run (locked wording): `"partial setup — origin configured but sync state=Unset (accept_flow likely failed at settings write); re-run /spt:psyche-sync-setup to converge."`
- **D-08:** Recovery lives **inline in SKILL.md** — no sibling doc/ADR. Depth = minimal + pointer. Order: (a) re-run setup (idempotent), (b) `$OWL doctor`, (c) `--disable` escape hatch + re-setup. `seed/` inspection + manual `rebase` = one-line last-resort pointer ONLY.
- **D-09:** Issue 7 **in scope for 35.3.** Non-git host projects never sync their `psyches/tracked/projects/<name>/` context. Sync-COVERAGE bug, distinct from 35.2's data-LOSS bug.
- **D-10:** Root cause was UNCONFIRMED — **now CONFIRMED in this research (see Issue 7 section).**
- **D-11:** Fix intent: a non-git host project MUST still get its tracked context synced. Detection must not gate tracked-context sync on host-repo presence.
- **D-12:** Add a lightweight unit test asserting each `SyncError`/`GitError` Display variant produces no `{`/Debug-struct syntax. Full snapshot fixture not required.

### Claude's Discretion
- D-03..D-08 are Claude's calls (recorded above). Researcher/planner MAY refine probe mechanics (D-04) and exact row wording (D-07) if a better approach surfaces, but lean/no-schema-change and Warn-severity intents are locked.

### Deferred Ideas (OUT OF SCOPE)
- **`accept_flow_attempt_ts` in SyncSettings** — pre-push attempt timestamp to distinguish "never attempted" from "attempted but write failed." Deferred to avoid a settings-schema change under shared-users backward-compat. Revisit if the D-03 probe proves insufficient.
- **Scope-shift contingency:** If Issue 7 root-cause revealed it was larger than a localized gating fix, split to a new phase (35.4). **This research finds the fix IS localized — contingency NOT triggered.**
- **OUT (handled by 35.2):** data-loss reconciliation in `accept_flow`, deterministic bootstrap in `ensure_seed`, per-ref push semantics. Do not re-touch.
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| SYNC-ERR-DISP-01 | Flip `{:?}`→`{}` so `SyncError` Display surfaces instead of Debug-repr leak | Confirmed: error site at `psyche_sync_setup.rs:75`; `SyncError` Display (`sync.rs:410-423`, 6 variants) + nested `GitError` Display (`git.rs:447-456`, 4 variants) both clean. |
| SYNC-DOCTOR-PARTIAL-01 | Doctor Warn row when `state==Unset` but origin configured + reachable | Confirmed: insertion point is the `SyncState::Unset` arm in `check_sync_status` (`doctor.rs:1144-1146` detail, `:1137` status). Reuse `run_git_with_timeout` (`git.rs:555`) for the bounded `ls-remote`. |
| SYNC-DOC-EXIT1-01 | Document exit code 1 in SKILL.md | Confirmed: `SKILL.md:88-103` "Interpret the exit code" lists 0/5/2/3/4; exit 1 (`psyche_sync_setup.rs:76`) is undocumented. |
| SYNC-DOC-RECOVERY-01 | Inline recovery doc in SKILL.md | Confirmed: SKILL.md has `## To disable` (`:116`) and `## Caveats` (`:127`) — recovery subsection slots between or under Caveats. |
| (Issue 7, no formal REQ-ID) | Non-git host projects must still sync tracked project context | **Root cause CONFIRMED:** `project_name_from_cwd_path` strict git-ancestor-or-None contract (`owlery.rs:1039-1061`). Fix is localized. See Issue 7 section. |

**Recommendation:** Assign Issue 7 a REQ-ID (e.g., `SYNC-NONGIT-PROJ-01`) in ROADMAP during planning so it tracks like the others.
</phase_requirements>

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Error string rendering to operator | Binary (owl.exe) dispatch | — | `psyche_sync_setup.rs::run` owns the `eprintln!` + exit code. |
| Doctor partial-state probe | Binary (doctor.rs) | git CLI subprocess | Read-only diagnostic; probes `seed/.git/config` + `git ls-remote`. |
| Exit-code / recovery contract | Skill layer (SKILL.md) | — | SKILL.md is the documented contract Claude reads to interpret exit codes. |
| Project-name resolution | Binary (owlery.rs resolver) | git CLI (ancestor walk) | `project_name_from_cwd_path` is the single source of truth for cwd→project-name; all tracked-context write sites consume it. |
| Tracked-context worktree + commit | Binary (tracked.rs) | shared `seed/` git repo | `ensure_project_worktree` + `commit_project_payload`; git-versioned regardless of host project. |

**Key tier observation for Issue 7:** the tracked-project worktree lives in the shared `seed/` repo and is git-versioned *independent of the host project's git status* — exactly as D-10 noted. The bug is therefore NOT in the sync/storage tier. It is upstream, in the project-name *resolution* tier: a non-git host never produces a project name, so the storage tier is never invoked for the project slice.

## Standard Stack

No new dependencies. This phase touches existing modules only.

| Module | Role in this phase |
|--------|--------------------|
| `src/owl/psyche_sync_setup.rs` | D-01 error-print flip (line 75). |
| `src/common/sync.rs` | `SyncError` Display (verify only, no change). |
| `src/common/git.rs` | `GitError` Display (verify only); `run_git_with_timeout` reuse for D-04 probe. |
| `src/owl/doctor.rs` | D-03..D-07 Warn row in `check_sync_status`. |
| `src/common/owlery.rs` | **Issue 7 fix** in `project_name_from_cwd_path` (line ~1043). |
| `src/common/tracked.rs` | `validate_id_chars` (reuse for Issue 7 fallback guard). |
| `plugin/spt/skills/psyche-sync-setup/SKILL.md` | D-02 exit-1 bullet + D-08 recovery subsection. |

**Installation:** none — no package changes.

## Package Legitimacy Audit

Not applicable — this phase installs no external packages. All work is within the existing Rust crate.

## Architecture Patterns

### Issue 7 — Project-Context Sync Data Flow (the bug, traced)

```
$LIVE start / commune / signoff
        │
        ▼
 route_two_slice_*  (echo_commune.rs)
        │  parses live slice + project slice from raw payload
        ├─ live slice  ──► ensure_agent_worktree(self_id) ──► write live_context.md ──► commit ──► (syncs OK)
        │
        └─ project slice
                │
                ▼
        resolve_self_project_name_via_info_cwd(self_id)   (owlery.rs:1094)
                │  reads info.json.cwd
                ▼
        project_name_from_cwd_path(&cwd)                  (owlery.rs:1039)  ◄── THE GATE
                │
                ├─ find_git_root_basename(cwd) == Some(name)  ──► Some(name)
                │
                └─ NO .git ancestor  ──► return None  ◄────────────  BUG: non-git host stops here
                         │
                         ▼
                SliceWriteState::SkippedNoProjectName     (echo_commune.rs:499 / 595)
                         │
                         ▼
                ensure_project_worktree NEVER called → no projects/{name}/ worktree
                → no commit on p-{name} → nothing on that branch to push → silent no-backup
```

**The fix (recommended, localized):** in `project_name_from_cwd_path` step (b), instead of unconditionally returning `None` when no `.git` ancestor is found, fall back to `cwd.file_name()` basename — guarded by `validate_id_chars` and the existing `tracked`/psyche_dir rejection — mirroring `derive_current_repo_names` step (c) (`owlery.rs:982-985`).

### Pattern: timeout-bounded git probe (for D-04)
**What:** All doctor git probes use a fixed-timeout wrapper so doctor never hangs.
**Use:** For the D-04 `git ls-remote origin` probe, reuse `run_git_with_timeout(&["ls-remote", "origin"], seed_dir)` from `git.rs:555` (returns `Option<String>`; `None` on timeout/nonzero/spawn-fail). Doctor's existing probes use a 500ms `DOCTOR_TIMEOUT_MS` (`tracked.rs:772`); match that posture. On `None`, degrade to "origin configured locally, remote unverified" per D-04 — do not block.
**Source:** `src/common/git.rs:555` (verified signature), `src/common/tracked.rs:770-772` (doctor timeout precedent).

### Anti-Patterns to Avoid
- **Re-touching `accept_flow` / `ensure_seed` / push semantics** — explicitly OUT (35.2 territory). Issue 7's fix is upstream of all sync logic; do not edit `sync.rs` push paths.
- **Reintroducing the synthetic-name vulnerability `project_name_from_cwd_path` was hardened against** — Phase 25.3 Defect B2 deliberately removed `file_name()` synthesis to prevent inventing a project for psyche_dir-internal paths. The fix MUST keep the `tracked`/psyche_dir rejection (step c) and add `validate_id_chars` so a hostile/odd basename cannot become a branch name (path-traversal / branch-metachar guard — same threat `validate_id_chars` already mitigates for IDs, `tracked.rs:143-152`).
- **Schema changes to SyncSettings** — banned by D-03/D-05 and the shared-users backward-compat hard constraint.
- **Making the doctor probe fatal** — D-06: Warn only; don't change doctor's exit semantics.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Bounded `ls-remote` probe (D-04) | Raw `Command` + manual kill/wait | `git::run_git_with_timeout` (`git.rs:555`) | Already handles timeout, zombie-safe wait, `hide_window` on Windows. |
| Project-name basename validation (Issue 7) | New regex/validator | `tracked::validate_id_chars` (`tracked.rs:149`) | Already the locked path-traversal/branch-metachar guard. |
| Non-git fallback semantics (Issue 7) | New resolution policy | Copy the proven D-06 fallback shape from `derive_current_repo_names` (`owlery.rs:982-985`) | Sibling resolver already solved "non-git cwd → basename" correctly; consistency + reuse. |
| Error Display (D-01) | New formatting code | Existing `SyncError`/`GitError` Display impls | Already complete and human-readable; the `{}` flip just routes through them. |

**Key insight:** Every sub-task here is "route through an existing, already-hardened primitive." The phase adds essentially no new logic — it removes a leak (D-01), surfaces existing state (D-03), documents existing behavior (D-02/D-08), and relaxes one over-strict gate (Issue 7).

## Runtime State Inventory

This is a fix phase, not a rename/migration. No stored-data keys, service config, OS registrations, secrets, or build artifacts carry a string that changes here.

- **Stored data:** None — no keys/IDs change. Issue 7 fix causes *new* `projects/{name}/` worktrees to be created for non-git hosts going forward; existing installs simply start backing up the project slice they were silently dropping. No migration of existing records needed (the agent slice was always synced; the project slice was just absent).
- **Live service config:** None.
- **OS-registered state:** None.
- **Secrets/env vars:** None — verified by trace; no env var names touched.
- **Build artifacts:** Standard — `cargo build --release` regenerates `owl.exe`; deploy via `docs/DEPLOY.ps1` syncs binary + skills (SKILL.md change must reach the plugin cache — covered by the standard all-files deploy).

## Common Pitfalls

### Pitfall 1: Fixing the wrong layer for Issue 7 (the gh red herring)
**What goes wrong:** Chasing the user's "gh false negative" framing and editing `gh_present()` / `should_emit_sync_prompt()` / live-start gating.
**Why it happens:** The user's symptom report names gh detection; the CONTEXT investigation pointers also list the prompt gate.
**How to avoid:** The prompt gate (`sync_prompt.rs:52-66`) controls only whether the *AskUserQuestion offer* fires; it never touches tracked-context writing, and it does NOT consult host git at all (it gates on `gh_present` + sync state). The fix belongs in `project_name_from_cwd_path` (`owlery.rs:1039`). Verified: no live-start path conflates host-git presence with sync eligibility.
**Warning signs:** Any diff touching `sync_prompt.rs`, `gh_present`, or `start.rs:422-432` for Issue 7 is on the wrong layer.

### Pitfall 2: Removing the psyche_dir/`tracked` guard while relaxing step (b)
**What goes wrong:** A naive "just return the basename" reintroduces Phase 25.3 Defect B2 (inventing a project named `tracked` for a psyche-internal cwd).
**How to avoid:** Keep step (c) rejection. Apply `validate_id_chars` to the basename before returning. Add a regression test that a psyche_dir-internal cwd still returns `None`.
**Warning signs:** `project_name_from_cwd_path_rejects_psyche_dir_basename` (existing test, `owlery.rs:4032`) fails after the change.

### Pitfall 3: Windows console-flash on the new ls-remote probe
**What goes wrong:** A raw `Command::new("git")` without `hide_window` flashes a console window on Windows.
**How to avoid:** `run_git_with_timeout` already applies `hide_window` — use it (this is also why D-04 says reuse the helper).

### Pitfall 4: SKILL.md change not reaching the plugin cache
**What goes wrong:** Editing `plugin/spt/skills/.../SKILL.md` in-repo but the live plugin reads from `~/.claude/plugins/cache/cplugs/spt/`.
**How to avoid:** Deploy via `docs/DEPLOY.ps1` (syncs skills); never hand-edit the cache. Per project memory: always full-sync on deploy.

## Code Examples

### D-01 error-print flip (verified site)
```rust
// src/owl/psyche_sync_setup.rs:74-77 — CURRENT (Debug-repr leak)
Err(e) => {
    eprintln!("sync setup failed: {:?}", e);   // {:?} → {}
    std::process::exit(1);
}
```
`SyncError` Display (`sync.rs:410-423`) renders e.g. `git push failed during seeding: git: nonzero exit: <stderr>` — clean. `GitError::Nonzero` Display (`git.rs:452`) trims stderr. No Display changes needed.

### Issue 7 fix shape (recommended — for planner, not to apply here)
```rust
// src/common/owlery.rs — project_name_from_cwd_path step (b), CURRENT:
None => {
    let _exists = path.exists();
    return None;                       // ← starves non-git host projects
}

// PROPOSED: guarded basename fallback (mirrors derive_current_repo_names step c)
None => {
    match path.file_name().and_then(|s| s.to_str()) {
        Some(name)
            if crate::common::tracked::validate_id_chars(name)
                && !(name == "tracked" && is_within_tracked_psyche_dir(path)) =>
        {
            return Some(name.to_string());
        }
        _ => return None,
    }
}
```
*(Planner refines; the `tracked`/psyche_dir guard and `validate_id_chars` gate are the non-negotiable parts.)*

### D-04 bounded ls-remote probe (verified helper)
```rust
// reuse src/common/git.rs:555
fn run_git_with_timeout(args: &[&str], cwd: &std::path::Path) -> Option<String>
// e.g. git::run_git_with_timeout(&["ls-remote", "origin"], seed_dir)
// None on timeout/nonzero/spawn-fail → degrade to "remote unverified" (D-04).
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `git update-ref`-against-bare-repo manual recovery | Per-ref dispatcher tags (`PUSHED`/`RECONCILED`/`DIVERGED`/`PUSH_FAILED`/`PROBE_FAILED`) | Phase 35.2 | D-08 recovery doc can be minimal + pointer; the old workaround is obsolete. |
| `project_name_from_cwd_path` synthesizing names via `file_name()` | Strict git-ancestor-or-None (Phase 25.3 Defect B2) | Phase 25.3 | Over-corrected: now starves non-git hosts. Issue 7 re-adds a *guarded* fallback — narrower than the original synthesis. |

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | The host project's cwd is the value stored in `info.json.cwd` and is what `resolve_self_project_name_via_info_cwd` reads | Issue 7 trace | If cwd is something else (e.g. always the psyche dir), Issue 7 manifests differently. LOW risk — verified the resolver reads `info.cwd` (`owlery.rs:1098`) and write sites consume the resolver; not separately verified against a live non-git repro. |
| A2 | No live-start/wrapper path computes the project name by a route that bypasses `project_name_from_cwd_path` | Issue 7 scope | If a bypass route exists with its own (correct) resolution, the fix may be partial. LOW–MEDIUM: grep shows all project resolution funnels through the shared resolver (`wrapper/mod.rs:735`, `echo_commune.rs`, `signoff.rs`), but I did not exhaustively trace every wrapper branch. Planner should spot-check `wrapper/mod.rs:4295,4381`. |

**Note:** All other claims are VERIFIED by direct source-trace in this session (file:line cited inline). Items A1–A2 are the only residual assumptions; both are low-risk and the planner can pin A2 with a 5-minute grep of `wrapper/mod.rs`.

## Open Questions (RESOLVED)

1. **Should the Issue 7 fallback also append a remote-origin basename (like `derive_current_repo_names` step b)?**
   - What we know: `derive_current_repo_names` adds a second name from `git remote get-url origin` for de-dupe in the `--here` filter.
   - What's unclear: for a single project worktree we need exactly ONE name. A non-git host has no origin anyway, so the remote step is moot for the bug case.
   - Recommendation: do NOT add the remote-basename branch — keep the fix to the minimal `.git`-walk-then-basename fallback. One project → one name.

2. **Existing live agents on non-git hosts that have been running pre-fix — do their accumulated project context need backfill?**
   - What we know: pre-fix, the project slice was never written to a worktree; the content lived only in whatever the agent wrote to its own context, not in `projects/{name}/`.
   - What's unclear: whether the project slice content is recoverable from anywhere.
   - Recommendation: out of scope for 35.3 (D-09 frames this as fixing forward coverage, not recovering lost backups). After the fix, the next commune/signoff writes the project slice and it syncs. No backfill task needed; note in plan.

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| `cargo`/Rust toolchain | build | assumed (project standard) | — | — |
| `git` CLI | D-04 probe + Issue 7 ancestor walk + tests | assumed (runtime dep) | — | D-02 GitUnavailable fallback already handles absence |
| `gh` CLI | NOT needed for any 35.3 work item | n/a | — | n/a (only the prompt offer uses it; untouched here) |

No blocking missing dependencies. The phase is code + docs only.

## Validation Architecture

This is a polish + one-gating-fix phase; validation is lightweight. Existing infra (`cargo test`, unit tests in-module, integration crates in `tests/`) covers everything. No new framework.

### Phase Requirements → Test Map
| Req | Behavior | Test Type | Where | Exists? |
|-----|----------|-----------|-------|---------|
| SYNC-ERR-DISP-01 (D-12) | Each `SyncError`/`GitError` Display variant has no `{` / Debug-struct syntax | unit | new `#[cfg(test)]` in `sync.rs` (and/or `git.rs`) | ❌ Wave 0 |
| Issue 7 | Non-git cwd → `project_name_from_cwd_path` returns `Some(basename)` (guarded) | unit | new test beside `owlery.rs:3997` (`..._returns_none_for_existing_non_git_dir` must be UPDATED — its current assertion is the bug) | ⚠️ existing test asserts the OLD (buggy) behavior — must be revised |
| Issue 7 (guard) | psyche_dir-internal cwd still returns `None` | unit | existing `project_name_from_cwd_path_rejects_psyche_dir_basename` (`owlery.rs:4032`) — must still pass | ✅ |
| Issue 7 (guard) | nonexistent path still returns `None` (or basename? decide) | unit | existing `..._returns_none_for_nonexistent_path` (`owlery.rs:4018`) | ⚠️ revisit: a nonexistent path has no basename worth syncing — keep `None` |
| SYNC-DOCTOR-PARTIAL-01 | `state==Unset` + origin configured + ls-remote ok → Warn row with locked wording | unit/integration | `source_order_doctor.rs` or new doctor test; mock/temp `seed/` with origin | ❌ Wave 0 |
| SYNC-DOC-EXIT1-01 / RECOVERY-01 | SKILL.md contains exit-1 bullet + recovery section | manual / `skill_hints.rs`-style assertion | doc check | optional |

**CRITICAL Wave 0 callout:** the existing unit test `project_name_from_cwd_path_returns_none_for_existing_non_git_dir` (`owlery.rs:3997-4015`) **encodes the bug as expected behavior**. The Issue 7 fix WILL break it — that break is correct and the test must be rewritten to assert the new guarded-fallback contract. The planner must flag this so the change isn't mistaken for a regression. Two sibling tests (`..._returns_none_for_nonexistent_path`, `..._rejects_psyche_dir_basename`) must continue to pass unchanged — they pin the guards.

### Commands
- Quick: `cargo test owlery::tests::project_name_from_cwd_path` and `cargo test --lib sync`
- Full: `cargo test`

### Wave 0 Gaps
- [ ] Display-no-debug-syntax unit test (D-12) in `sync.rs`/`git.rs`
- [ ] Revise `project_name_from_cwd_path_returns_none_for_existing_non_git_dir` → assert `Some(basename)`
- [ ] New doctor partial-state Warn-row test
- [ ] Cross-platform note: golden tests are Windows-CI-excluded; keep new tests platform-neutral (no bash-only fixtures).

## Security Domain

Minimal surface. The one security-relevant change is the Issue 7 fallback, which reopens a path-derived string into a git branch name (`p-{name}`).

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V5 Input Validation | yes | `validate_id_chars` on the fallback basename before it becomes a branch name (`[A-Za-z0-9_-]+`, no `..`, no metachars). Already the locked guard (T-24-01-01 mitigation). |
| V6 Cryptography | no | — |
| V2/V3/V4 | no | — |

| Pattern | STRIDE | Mitigation |
|---------|--------|------------|
| Path traversal / branch-metachar injection via host dir name (Issue 7 fallback) | Tampering | `validate_id_chars` gate (reuse, do not hand-roll) — reject names with `..`/`/`/shell metachars before `git worktree add -b p-{name}`. |

## Sources

### Primary (HIGH confidence — direct source-trace this session)
- `src/common/owlery.rs:965-1100` — `derive_current_repo_names`, `find_git_root_basename`, `project_name_from_cwd_path`, `resolve_self_project_name_via_info_cwd` (Issue 7 root cause).
- `src/owl/echo_commune.rs:485-620` — project-slice write sites; `SkippedNoProjectName` on `None`.
- `src/owl/sync_prompt.rs:1-154` — prompt gate (gh + state only; no host-git, no context write).
- `src/common/sync.rs:163-195` (`gh_present`), `400-423` (`SyncError` Display).
- `src/common/git.rs:409-418` (`git_project_basename`), `435-456` (`GitError` Display), `555` (`run_git_with_timeout`).
- `src/owl/psyche_sync_setup.rs:55-78` — error site + exit codes.
- `src/owl/doctor.rs:1130-1184` — `check_sync_status` (Unset arm).
- `src/common/tracked.rs:1-120, 143-152, 700-790` — `TrackedError`, `validate_id_chars`, `ensure_project_worktree`.
- `plugin/spt/skills/psyche-sync-setup/SKILL.md:88-137` — exit-code + caveats sections.
- `.planning/debug/sync-setup-data-loss.md`, `.planning/seeds/...`, `35.3-CONTEXT.md`, `.planning/ROADMAP.md` §35.3.

### Secondary
- `CHANGELOG.md:444-451` — Phase 24 note "`ensure_project_worktree` ... no caller invokes it in v1.8" (historical context for when project-slice wiring landed).

## Metadata

**Confidence breakdown:**
- Issue 7 root cause: HIGH — exact gating line traced, write-site `None` handling confirmed, sibling resolver contrast confirmed.
- Issue 7 in-scope/localized verdict: HIGH — fix is ~10 lines in one function + test updates; no schema, no new surface, contingency-split NOT triggered.
- Four polish items: HIGH — all touchpoints confirmed at current file:line; mechanical.
- Residual risk: only A1/A2 (low) — recommend planner spot-check `wrapper/mod.rs` for a bypass resolution route.

**Research date:** 2026-05-29
**Valid until:** ~2026-06-28 (stable internal codebase; re-verify line numbers if other phases edit these files first).
