---
phase: 24-tracked-dir-forked-repo-layout-agents-projects-branches-sess
plan: 06
subsystem: doctor + tracked
tags: [doctor, d-17, d-02-fallback, d-05-prune-quirk, pattern-5, sc10]
requires:
  - Plan 24-01 (ensure_seed, seed_path, GitError, run_git_checked, gc.worktreePruneExpire=never config)
  - Plan 24-02 (ensure_agent_worktree, ensure_project_worktree, agent_worktree_path, agent_branch)
  - Plan 24-03 (commit_agent_payload — used by doctor_status_local_only_count test)
provides:
  - "`crate::common::tracked::doctor_status_rows() -> DoctorStatus` — read-only worktree state collector (pub fn)"
  - "`crate::common::tracked::DoctorStatus` / `WorktreeRow` / `WorktreeScope` / `WorktreeState` — public data shapes for doctor surface"
  - "`crate::common::tracked::parse_worktree_porcelain` / `reverse_map_branch` / `parse_prune_orphans` — pub(crate) PURE parsers"
  - "`crate::owl::doctor::check_tracked_layout(fix) -> Vec<DiagResult>` — wired into both run(fix) and doctor_result(fix)"
affects:
  - src/common/tracked.rs
  - src/owl/doctor.rs
tech-stack:
  added: []
  patterns:
    - "Public data-shape vs internal-helper enum split: public `WorktreeScope { Agent, Project, Bare }` (doctor surface) coexists with renamed internal `EnsureScope { Agent, Project }` (worktree creator). Avoids name collision while keeping the internal creator type-narrow (never creates Bare)."
    - "Dual-source orphan detection: `worktree list --porcelain` `prunable {reason}` markers AS canonical source + `worktree prune --dry-run --verbose` as defensive supplement. The locked `gc.worktreePruneExpire=never` config (D-05) silences `worktree prune` on git 2.43/Windows; the porcelain `prunable` line is git's own in-band signal and is reliable in that case."
    - "PURE parser pairs (struct + parse fn) for `worktree list --porcelain` and `worktree prune --dry-run --verbose` outputs. Each is unit-tested without filesystem touch — emoji-/CRLF-safe via String normalization."
    - "Bootstrap-commit subtraction in LocalOnly state: `rev-list --count HEAD` includes the seed's `init: tracked seed` commit which is reachable from every agent/project worktree; saturating-sub by 1 yields post-bootstrap commits the operator can act on."
    - "--fix orphan prune uses `--expire=now` flag to override the locked `gc.worktreePruneExpire=never` config — without this flag the prune is a silent no-op on git 2.43/Windows (empirically verified)."
key-files:
  created: []
  modified:
    - "src/common/tracked.rs (+660 lines: new public types DoctorStatus / WorktreeRow / WorktreeScope / WorktreeState; pub(crate) parsers parse_worktree_porcelain / reverse_map_branch / parse_prune_orphans; pub fn doctor_status_rows; internal enum rename WorktreeScope -> EnsureScope; 13 new tests)"
    - "src/owl/doctor.rs (+338 lines: new fn check_tracked_layout(fix); wired into run() and doctor_result(); 4 new tests + EnvSnapshot/ENV_LOCK fixtures)"
decisions:
  - "Use the `prunable` marker from `worktree list --porcelain` as the canonical orphan source, not `worktree prune --dry-run` alone. Discovered during TDD: the locked D-05 config (`gc.worktreePruneExpire=never`) silences `worktree prune --dry-run --verbose` on git 2.43/Windows even though `worktree list --porcelain` correctly emits `prunable gitdir file points to non-existent location` for the orphan. Both sources are merged (deduplicated) in doctor_status_rows for defensive coverage on older git."
  - "`seed not yet initialized` emits PASS (not INFO/WARN). The `DiagStatus` enum has only Pass/Warn/Fail variants — adding INFO would touch every other check site. PASS is the right choice anyway: per D-16, lazy worktree creation means a virgin SPT_HOME genuinely has nothing tracked yet, and the next commune/signoff will materialize it. Operators don't need a yellow flag for normal pre-state."
  - "`LocalOnly { commits }` renders as PASS (`{N} local`), not WARN. Per D-13 amendment, Phase 24 has no remote and 'local commits' is the steady state for every worktree. Phase 35 will add the push step and re-categorize this state. Until then, surfacing it as WARN would create permanent yellow rows for healthy agents."
  - "Rename internal `enum WorktreeScope { Agent, Project }` to `EnsureScope` to free the public `WorktreeScope` namespace. The internal enum only ever creates non-bare worktrees so it stays two-variant; the public enum needs three variants (Agent / Project / Bare) to model the seed's `bare` row. Two enums with overlapping names would be confusing in test output and panic messages."
  - "Subtract the bootstrap commit from the LocalOnly count via saturating_sub. `rev-list --count HEAD` includes `init: tracked seed` which is the merge-base of every agent/project worktree (per D-06). Surfacing `1 local` for a clean fresh worktree would be misleading; `0 local` (collapsed back to Clean) is the correct read."
  - "`--fix` mode uses `git worktree prune --expire=now` (with the flag), not bare `git worktree prune`. The flag is required to override the D-05 locked config; otherwise --fix is a silent no-op on the path that doctor reports as fixable. The bash-test verification of the original RESEARCH-quoted command shape was misleading because the bash repro didn't replicate the locked config exactly."
  - "`check_tracked_layout` lives at the END of the run()/doctor_result() dispatch order, not interleaved between existing checks. Operators reading the diagnostic output see existing layout/perch/registry/pid/spool rows first (the legacy diagnostic surface) followed by the new tracked: rows. Stable ordering helps muscle-memory."
  - "Doctor row name uses underscore-free colon-separated `tracked:agent:{name}` and `tracked:project:{name}` to mirror RESEARCH Pattern 5 line 380. Detail shape `{name} → {branch} → {state}` uses U+2192 (→) per RESEARCH; Windows + Unix UTF-8 render identically."
metrics:
  duration_minutes: 35
  completed: 2026-05-20
  commits: 2
  tests_added: 17  # 13 in tracked.rs + 4 in doctor.rs
  tests_modified: 0
  files_created: 0
  files_modified: 2
---

# Phase 24 Plan 06: D-17 Doctor Tracked-Layout Surface Summary

One-liner: `owl.exe doctor` and `$LIVE doctor` now print a per-worktree status row for every linked worktree under `psyches/tracked/seed/` — `[PASS] tracked: seed initialized at {path}` followed by `[PASS|WARN] tracked:{scope}:{name} → {branch} → {clean|dirty (N modified)|N local}` rows, plus `[WARN] tracked: orphan worktree at seed/worktrees/{name}` rows for any stale admin entries (`--fix` runs `git worktree prune --expire=now` to clean them up). Read-only diagnostic on the happy path; never blocks payload delivery. Pre-Phase-24 SPT_HOMEs (no `seed/` yet) emit a single `[PASS] tracked: not yet initialized (will auto-init on first commune/signoff)` row.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | `tracked::doctor_status_rows()` — read-only worktree state collector | `464ea96` | `src/common/tracked.rs` |
| 2 | `check_tracked_layout` integration into doctor.rs::run + doctor_result | `b0fcb64` | `src/owl/doctor.rs` |

## Decisions Made

1. **`prunable` line is the canonical orphan source.** Discovered during TDD: `gc.worktreePruneExpire=never` (D-05) silences `worktree prune --dry-run` on git 2.43/Windows. `worktree list --porcelain` emits `prunable gitdir file points to non-existent location` reliably. Doctor uses both sources, deduplicated.
2. **`seed not yet initialized` is PASS, not INFO/WARN.** Per D-16 lazy creation, virgin SPT_HOME has nothing tracked yet — that's expected, not yellow-worthy. PASS also avoids extending the `DiagStatus` enum.
3. **`LocalOnly { commits }` renders as PASS.** Per D-13 amendment Phase 24 has no remote; "local" is the healthy steady state. WARN would create permanent yellow rows.
4. **Internal `WorktreeScope` renamed to `EnsureScope`.** Frees the public namespace for the new doctor type. Internal creator stays two-variant (never creates Bare).
5. **Subtract bootstrap commit from LocalOnly count.** `rev-list --count HEAD` includes `init: tracked seed`; surfacing `1 local` on a fresh worktree would be misleading.
6. **`--fix` uses `--expire=now` flag explicitly.** Required to override D-05 locked config — without it, `git worktree prune` is a silent no-op on the very orphans doctor reports as fixable.
7. **Doctor row name uses `tracked:{scope}:{name}` colon-separated identifiers.** Mirrors RESEARCH Pattern 5 shape verbatim. Detail uses U+2192 (→) — Windows + Unix UTF-8 render identically.

## Verification

### Automated

```bash
$ cargo test --lib common::tracked::tests::doctor_status -- --test-threads=1
test common::tracked::tests::doctor_status_dirty_worktree ... ok
test common::tracked::tests::doctor_status_initialized_one_agent ... ok
test common::tracked::tests::doctor_status_local_only_count ... ok
test common::tracked::tests::doctor_status_orphan_detected ... ok
test common::tracked::tests::doctor_status_porcelain_parser_handles_crlf ... ok
test common::tracked::tests::doctor_status_porcelain_parser_prunable ... ok
test common::tracked::tests::doctor_status_porcelain_parser_unit ... ok
test common::tracked::tests::doctor_status_prune_orphans_parser ... ok
test common::tracked::tests::doctor_status_prune_orphans_parser_empty_on_clean_repo ... ok
test common::tracked::tests::doctor_status_reverse_map_agent ... ok
test common::tracked::tests::doctor_status_reverse_map_project ... ok
test common::tracked::tests::doctor_status_reverse_map_unknown_branch_skipped ... ok
test common::tracked::tests::doctor_status_uninitialized_seed ... ok

test result: ok. 13 passed; 0 failed

$ cargo test --lib owl::doctor::tests -- --test-threads=1
test owl::doctor::tests::check_tracked_layout_dirty_warns ... ok
test owl::doctor::tests::check_tracked_layout_one_agent_clean ... ok
test owl::doctor::tests::check_tracked_layout_orphan_warns_and_fix_prunes ... ok
test owl::doctor::tests::check_tracked_layout_uninitialized ... ok

test result: ok. 4 passed; 0 failed

$ cargo test --lib -- --test-threads=1
test result: ok. 639 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out
```

### Manual

`SPT_TRAMPOLINE_GUARD=1 ./target/release/owl.exe doctor` on the developer's working SPT_HOME (no `psyches/tracked/seed/` yet — pre-Phase-24 state):

```
doctor running diagnostics...
  [PASS] spt layout: all subdirectories present at C:\Users\decid\AppData\Local\spt
  [PASS] env vars: $OWL and $LIVE both set
  [WARN] perch:doyle-w270: stale (pid + parent_pid dead)
  [WARN] registry: 5 alive, 9 stale
  [WARN] pid:.psyche-wrapper-executor.pid: dead pid 52912
  [WARN] pid:.psyche-wrapper-mica.pid: dead pid 86012
  [WARN] pid:.psyche-wrapper-witty.pid: dead pid 20416
  [WARN] spool:doyle: 243 messages, 51KB -- consider draining
  [WARN] spool:doyle-psyche: 228 messages, 39KB -- consider draining
  [WARN] spool:dunsen: 154 messages, 29KB -- consider draining
  [PASS] tracked: not yet initialized (will auto-init on first commune/signoff)

  Summary: 3 pass, 8 warn, 0 fail
  Run with --fix to auto-clean issues.
```

The new `[PASS] tracked: ...` row appears as the last line of the per-check block. Trampoline guard env var is set explicitly because the build-from-this-repo `owl.exe` trampolines to the installed plugin binary by default (Phase 18.4 handoff D-09/D-10); the env var inhibits that one-shot exec so the just-built binary runs.

Once a first commune/signoff lands (Plan 24-03/24-04 wire-up), the surface will switch to:

```
  [PASS] tracked: seed initialized at C:\Users\decid\AppData\Local\spt\psyches\tracked\seed
  [PASS] tracked:agent:doyle → a-doyle → clean
  [PASS] tracked:agent:doyle → a-doyle → 3 local      # after 3 communes
  [WARN] tracked:agent:doyle → a-doyle → dirty (1 modified)
  [WARN] tracked: orphan worktree at seed/worktrees/zombie (run --fix to prune)
```

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] `worktree prune --dry-run --verbose` silent on D-05-configured seed**
- **Found during:** Task 1 TDD (test `doctor_status_orphan_detected` failed on first run)
- **Issue:** RESEARCH Pattern 5 (line 386) prescribed parsing `worktree prune --dry-run --verbose` output to detect orphans. Empirically on git 2.43.0.windows.1, the locked `gc.worktreePruneExpire=never` config (D-05, set by `ensure_seed` at init time) causes `worktree prune --dry-run` to return empty output for orphans that ARE flagged `prunable` in `worktree list --porcelain`. The two git subcommands disagree under the D-05 config — RESEARCH did not anticipate this interaction.
- **Fix:** Treat the `prunable {reason}` line in `worktree list --porcelain` as the canonical orphan signal. Added a `pub(crate) prunable: bool` field to `RawWorktreeEntry` and a `parse_worktree_porcelain` test (`doctor_status_porcelain_parser_prunable`) asserting it parses correctly. Still also consume `parse_prune_orphans` output and merge both sources (deduplicated) so older git versions where the porcelain output lacked `prunable` still work.
- **Files modified:** `src/common/tracked.rs`
- **Commit:** `464ea96`

**2. [Rule 1 - Bug] `--fix` requires `--expire=now` to override D-05 config**
- **Found during:** Task 2 TDD (test `check_tracked_layout_orphan_warns_and_fix_prunes` failed on first run — the `--fix` call ran `git worktree prune` and reported success but the orphan persisted)
- **Issue:** Plan action step C.8 said `invoke 'git -C {seed} worktree prune'` — same underlying issue as Bug 1. Without `--expire=now`, the locked `gc.worktreePruneExpire=never` config makes the prune a silent no-op.
- **Fix:** `--fix` orphan prune uses `--expire=now` explicitly. Documented inline in `check_tracked_layout` source and in Decision #6 above.
- **Files modified:** `src/owl/doctor.rs`
- **Commit:** `b0fcb64`

**3. [Rule 2 - Critical] LocalOnly count includes bootstrap commit**
- **Found during:** Task 1 TDD (test `doctor_status_initialized_one_agent` initially expected Clean state but got `LocalOnly { commits: 1 }`)
- **Issue:** `rev-list --count HEAD` on a freshly-created `worktree add ../agents/doyle -b a-doyle main` returns 1, because the seed's bootstrap commit (`init: tracked seed`, D-06) is reachable from the new branch's HEAD. Plan's behavior section described LocalOnly as "every commit on HEAD counts" — but the bootstrap commit is shared across every worktree, so surfacing `1 local` on a virgin worktree is misleading (operators read "this worktree has 1 unpushed commit" — false; it's the shared bootstrap commit they cannot meaningfully push).
- **Fix:** `saturating_sub(1)` from the rev-list count. Collapses to Clean when the post-bootstrap count is zero.
- **Files modified:** `src/common/tracked.rs`
- **Commit:** `464ea96`

**4. [Rule 3 - Blocking] Internal `WorktreeScope` enum collides with new public name**
- **Found during:** Task 1 setup (before writing any test)
- **Issue:** Plan-prescribed public type `pub enum WorktreeScope { Agent, Project, Bare }` collided with the existing internal `enum WorktreeScope { Agent, Project }` in `src/common/tracked.rs` (line 289 pre-rename). Plan action step A said "add structs + enums per behavior section" — silent on the collision.
- **Fix:** Renamed the internal enum to `EnsureScope` (it's only used by `ensure_worktree(...)` which already creates a `Scope`-typed parameter). Public `WorktreeScope` now has Bare variant.
- **Files modified:** `src/common/tracked.rs`
- **Commit:** `464ea96`

### Auth Gates

None.

### Architectural Changes (Rule 4)

None — plan executed within its prescribed surface.

## Known Stubs

None.

## TDD Gate Compliance

This plan is `type: execute` (not `type: tdd`), so the plan-level RED→GREEN→REFACTOR gate sequence does not apply. Individual tasks were tagged `tdd="true"`; both followed the per-task TDD discipline:

- **Task 1:** Tests + implementation landed together in commit `464ea96` (single-feature pair). Tests were validated by running them — `doctor_status_orphan_detected` failed on the first run (Bug 1 above), confirming the test was real RED before the porcelain-prunable fix took it to GREEN. The other 12 tests passed first try, indicating the rest of the implementation was correct on RED→GREEN single-pass.
- **Task 2:** Tests + implementation landed together in commit `b0fcb64`. `check_tracked_layout_orphan_warns_and_fix_prunes` failed on the first run (Bug 2 above), confirming RED→GREEN.

No `test(...)` commit was made separately; the executor harness allows paired commits when tests are colocated in the same Rust module as the code (per execute-plan.md TDD reference). Both Bug 1 and Bug 2 were caught by RED tests — TDD discipline functioned as designed.

## Self-Check: PASSED

- `[ -f .planning/phases/24-tracked-dir-forked-repo-layout-agents-projects-branches-sess/24-06-SUMMARY.md ]` → FOUND
- `git log --oneline | grep 464ea96` → FOUND (Task 1)
- `git log --oneline | grep b0fcb64` → FOUND (Task 2)
- `cargo test --lib -- --test-threads=1` → 639 passed (17 new green)
- Manual `owl doctor` smoke → new `[PASS] tracked: not yet initialized` row visible
