# Worktree salvage GC + od-wip restore — request

**Goal:** the existing worktree GC salvages every idle worktree's work to safety
(vault archive + `wip/*` push) before deleting it, and a new global `od-wip` CLI
restores any salvaged item in one command.

Authoritative design: `docs/specs/2026-08-14-worktree-salvage-gc-design.md` —
read it FIRST; it pins every policy decision, seam, and safety invariant. This
request doc is the executable summary; on any conflict the spec wins.

**Context:** `modules/monitor/lib/worktree_gc.py` (fail-closed GC, sole caller
`modules/monitor/slices/bin/disk-maintain` under the disk-check timer) today keeps
dirty worktrees forever and reaps clean ones at 7 days. Session liveness oracle:
installed `list-sessions` (`modules/workstation/claude/bin/list-sessions`).
Worktree claiming: `~/.claude/bin/od-worktree`.

**Files:**
- Modify `modules/monitor/lib/worktree_gc.py` — TTL 7d→3d; session-attached keep;
  archive-first salvage (`archive_worktree`, `salvage_push`, `live_session_cwds`,
  `cleanup_local_branch`, vault expiry); removal gate; per-repo
  `git fetch --prune origin` and `git worktree prune --expire=7.days`; extended
  summary JSON (`salvaged`, `freedKib`, `vaultKib`, `expiringSoon`).
- Modify `modules/monitor/slices/bin/disk-maintain` — pass `--vault-root` default,
  parse new JSON fields, `cleaned` token `worktree-gc:<reaped>(<salvaged>s)`,
  edge-triggered `notify-send` line (non-fatal on failure, distinct log line).
- Modify `modules/monitor/tests/test_worktree_gc.py` — coverage list in spec
  §Testing, verbatim.
- Create `modules/workstation/claude/bin/od-wip` — global list/restore CLI (spec
  §Restore primitives), beside `od-worktree`; plus install symlink registration
  the way sibling tools in that dir reach `~/.claude/bin`.
- Create `modules/workstation/claude/tests/od-wip.test.*` — real tmp-repo
  fixtures, same harness as the sibling `od-worktree-registry` test.
- Modify `CLAUDE.md` (repo root, worktree section) — 3 lines: salvage destination,
  `od-wip list` / `od-wip restore <ref>`, never dig manually.
- Modify `modules/workstation/claude/skills/finishing-a-development-branch/SKILL.md`
  — one standing rule line: cleanup automation must never delete origin `wip/*`.

**Contract (seams — full signatures and behavior in the spec):**
- `archive_worktree(worktree_path: str, vault_dir: Path, run_git: RunGit, clock: Clock) -> dict | None`
  — DELTA archive only: paths from `git status --porcelain --ignored=matching`
  minus `REPRODUCIBLE_IGNORED_DIRS`; clean tracked content is never archived.
- `salvage_push(worktree_path: str, branch_label: str, run_git_env: RunGitEnv) -> dict | None`
  where `RunGitEnv = Callable[[str, list[str], float | None, dict[str, str] | None], str | None]`
- `live_session_cwds() -> set[str] | None`
- `cleanup_local_branch(repo_root: str, branch: str, run_git: RunGit) -> bool`
- `expire_vault_entries(vault_root: Path, clock: Clock, ttl_ms: float) -> list[dict]`
- Vault root default `~/.local/state/overdeck/worktree-vault`; env
  `SM_WORKTREE_GC_VAULT_ROOT`, CLI `--vault-root`.
- Wip ref literal shape: `wip/<branch-label>-<YYYYMMDD>-<sha7>`; commit identity
  `worktree-gc <worktree-gc@overdeck.local>`; message
  `salvage: auto-commit idle worktree <branch-label>`.
- Vault entry dir literal shape:
  `<repo-name>--<branch-label>--<YYYYMMDD-HHMMSS>--<sha7>`, dirs 0700 files 0600.
- CLI: `od-wip list [--repo <path>] [--json]`,
  `od-wip restore <wip-ref | vault-entry-id> [--repo <path>]`.

**Behavior:** exactly the spec's policy + salvage steps 1–3 + removal gate +
error handling. Non-negotiable invariants: salvage never mutates the worktree
(temp index via `GIT_INDEX_FILE`, lstat-only archiving); removal requires
fsync'd manifest + live-verified push + identity/occupancy/session re-validation;
every failure path is a keep-with-reason journal entry; secrets
(`PRECIOUS_IGNORED_GLOBS` basenames) never enter a pushed tree; `od-wip restore`
refuses when the target worktree already exists.

**Out of scope:** no new systemd units/timers/crons; no buildbox changes; no
server-side branch protection; no UI page; no changes to `factory`/harness
modules; orphan unregistered `.worktrees` dirs journal-only; never weaken any
existing keep-reason.

**Acceptance:**
- Run: `python3 -m pytest modules/monitor/tests/ -q` — PASS, including every new
  case in spec §Testing.
- Run: the od-wip test target created above — PASS, including: restore
  reproduces tracked + untracked + secret files byte-identical; second restore
  refuses and prints the existing path.
- Run: `python3 modules/monitor/lib/worktree_gc.py --state-dir /tmp-safe-path`
  (dry-run, no `--apply`) against a fixture repo with a dirty 4-day-idle worktree
  — output JSON reports it under `would-salvage`, and `git status` in that
  worktree is byte-identical before/after.
- Grep: `rg -n 'wip/' modules/workstation/claude/skills/finishing-a-development-branch/SKILL.md`
  shows the never-delete rule.
