# Worktree reaper must never touch live sessions

audience: AI coding agents.

## Outcome
`modules/monitor/lib/worktree_gc.py` (salvage GC on `system-monitor-disk-check.timer`)
never deletes a worktree that a live session, a land-queue ticket, or an in-progress
checkout still owns — regardless of idle-age heuristics.

## Status
DONE — installed on this box; landed to `origin/main`.

## Source request
Owner FIRE: "the worktree reaper must be adjusted to never touch sessions that are
alive." Three confirmed victims:
1. A worktree with a queued land-ticket (`.git/harness/landq/queue`) removed before
   the conductor served it.
2. tool-registry lane worktree reaped mid-session, uncommitted pass lost.
3. `.worktrees/tmux-runtime-boundary` reaped while its session sat attached in tmux
   (salvaged to `wip/147c78fb7`, but the live session lost its working tree).

## Root cause per victim
1. **Land-queue ticket**: `worktree_gc.py` had no knowledge of `.git/harness/landq/`
   at all — no keep condition referenced it.
2. **tool-registry mid-session loss**: `worktree_idle_ms` only sampled the top-level
   512 directory entries (`os.scandir`, non-recursive) plus the git index mtime.
   Edits nested under subdirectories (e.g. `modules/foo/bar.py`) never touched the
   worktree root's own top-level entries or the index (no `git status`/`add` run
   mid-session), so idle-age looked stale despite live uncommitted work.
3. **tmux-attached session**: `live_session_cwds()` shelled out to the `list-sessions`
   binary and trusted its JSON blindly — no independent pid verification. A
   crashed/stale/phantom binary run, or a ledger row surviving process death,
   could both silently drop or falsely report a session as live.

## Acceptance criteria
- Keep conditions added (worktree UNTOUCHABLE if ANY holds): live-session cwd
  (pid-verified against `/proc`, ledger never trusted raw), live land-queue ticket
  (`.git/harness/landq/queue` + `ticket.*.job`, `wt` field, no terminal `.verdict`),
  fresh `index.lock` (checkout in progress), uncommitted changes/idle-age measured
  from the newest of recursive file mtime, git index, HEAD, and reflog.
- Fail-closed: any oracle read error keeps the worktree.
- One test per victim scenario + baseline `modules/monitor/tests/` green (was 264,
  now 273 with the new/fixed tests) + `modules/harness/factory/tests/` green
  (unaffected, 246 passing — sanity run per repo CLAUDE.md).
- Installed on this box before landing; proven via dry-run fixture.

## Preserved WIP / refs
- Branch: `wt/wt-reaper-liveness`, worktree `.worktrees/wt-reaper-liveness`.
- Changed: `modules/monitor/lib/worktree_gc.py`,
  `modules/monitor/tests/test_worktree_gc.py`,
  `modules/monitor/tests/test_disk_maintain.py` (`_age()` helper needed to also age
  `HEAD`/`logs/HEAD` once idle-age started sampling them — pre-existing test gap
  exposed by the correctness fix, not a new requirement).

## Constraints
- Never weaken the salvage half (still salvage-then-delete for genuinely dead trees).
- Never touch factory phase/retry/gate/permission/commit behavior.
- Worktree-only; no work in the shared main checkout.

## Execution steps
1. Read current keep-conditions, map each victim to its root cause. DONE.
2. Add pid-verified session-ledger read (`live_session_cwds` now reads
   `~/.local/state/agent-sessions/sessions/*.json` directly and checks
   pid + `pidStartTicks` against `/proc/<pid>/stat`, dropping the `list-sessions`
   subprocess dependency entirely). DONE.
3. Add land-queue ticket keep (`landq_live_worktree_paths`, checked both at initial
   gate and in the pre-archive/pre-remove recheck). DONE.
4. Add `index.lock` freshness keep (`index_lock_recent`, 15-minute window). DONE.
5. Fix `worktree_idle_ms` to scan recursively (budget-capped 4,000 files) and sample
   git `HEAD` + `logs/HEAD` (reflog) in addition to `index`. DONE.
6. Tests: one per victim scenario + regression for the idle-scan blind spot. DONE
   (`test_keeps_worktree_with_live_landq_ticket`,
   `test_reaps_worktree_once_landq_ticket_has_terminal_verdict`,
   `test_keeps_worktree_with_recent_index_lock`,
   `test_keeps_worktree_with_pid_verified_live_session_ledger_entry`,
   `test_reaps_worktree_with_stale_ledger_entry_pid_mismatch`,
   `test_idle_ms_detects_nested_file_edit`).
7. Install before landing: deploy locally, prove the installed entrypoint keeps a
   fixture worktree with a fake live ticket (dry-run).
8. Land via `.claude/scripts/ship.sh` from the repo root.

## Current receipt
All steps done. 273/273 monitor tests green, 246/246 factory tests green (sanity).
Landed direct to `origin/main` at `dd0b3ac5b` (merge of `wt/wt-reaper-liveness`,
fix commit `7d3a31127`). Deployed via `packaging/deploy-local.sh`
(`sha":"356a744e"`); installed `~/.local/share/overdeck/deploy/modules/monitor/lib/worktree_gc.py`
byte-matches the landed commit. Proved live: ran the installed entrypoint against
a fixture repo with an aged worktree carrying a fake live land-queue ticket
(`.git/harness/landq/ticket.*.job` + `queue`) — GC kept it (`kept: 1, reaped: 0`,
worktree still on disk). Did NOT re-verify against the other two victim classes
(session-ledger, index.lock) on the live installed binary — those are covered by
the 6 new unit tests only, not a second live fixture run (time/system-load budget).

## Next executable action
None — done. If a future GC run still reaps a live worktree, check
`~/.local/state/system-monitor/worktree-gc.jsonl` for the `reason` field first.
