git-worktree-teardown-missing-directory
The repository itself was not mounted in this environment (only /workspace/problem.json), and coding-hermes-tools is not publicly retrievable. So I reconstructed the fix from the specification and verified it end-to-end against real git 2.53.0 / go1.26.0, then wrote the deliverable to /workspace/solution.md.
Diagnossed & empirically confirmed every measured shape in throwaway repos:
- git -C <missing-wt> status --porcelain → exit 128 raw fatal
- stale .git/worktrees/<id> entry survives, still blocks git branch -d
- git worktree remove --force <missing> succeeds
- plain git worktree prune (no --expire) drops the entry immediately
- git branch -D works for commits existing nowhere else; base branch untouched
- discard works while root is on a different branch, HEAD unmoved
- live proof of the substring hazard: grep "wt-g1" matches wt-g10, while exact cleaned-path comparison correctly reports g1 gone
Built a runnable Go module mirroring internal/session/session.go + cmd/toolsd/main.go with the --discard flag, then ran 5 acceptance tests against real git — all pass:
- typed refusal keeps state intact
- --discard cleans entry+branch+record+leases, root unmoved
- dirty refusal unchanged, then discard works
- sibling g1/g10 not confused
- discard with different root branch
Black-box probe on the built binary reproduces the original symptom verbatim, then shows the fixed typed refusal (exit=2, state intact) and the successful --discard (exit=0, everything gone).
/workspace/solution.md (610 lines) contains:
1. Title + root-cause analysis
2. Measured git behavior table
3. The exact fix — full session.go code, main.go flag wiring, and the raw git command sequence
4. Verification — test output, black-box probe transcript, live substring-hazard demonstration, and a manual recovery one-liner
5. Acceptance checklist
Key fix points implemented: stat-before-git → typed MissingWorktreeError; worktree remove --force (present) / worktree prune (absent); branch -D; exact filepath.Clean post-condition against worktree list --porcelain; and registry/lease release only after entry+branch are proven gone and root HEAD unmoved.
# Evidence - Problem class: git-worktree-teardown-missing-directory - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T12:37:47.111Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a tool that owns a linked git worktree cannot tear it down once the worktree DIRECTORY has been deleted by hand (or lost with a container/volume). Every teardown path starts with a command that runs INSIDE the worktree ('git -C <wt> status --porcelain'), which dies with 'fatal: cannot change to <path>: No such file or directory' -- a raw failure that names neither the session nor the way out -- and the state the tool created (branch, registry record, leases) persists forever with no flag to clear it.\n\nROOT CAUSE: teardown was written for the shape where the directory is present. Git keeps the administrative entry under .git/worktrees/<id> after the directory is gone, so the repo still lists the worktree and still refuses branch deletion while that entry exists.\n\nFIX: (1) before any git command in the worktree, stat the path and answer a typed refusal that names the missing worktree AND the teardown verb (an explicit --discard), never a raw git error; (2) make the teardown itself tolerate the missing directory: `git worktree remove --force <path>` when the path exists (--force so a dirty worktree does not block a deliberate discard), then `git worktree prune` when the directory is already gone; (3) delete the branch with -D (the commits are unmerged by definition); (4) assert the post-condition by comparing CLEANED paths against `git worktree list --porcelain`, not substrings -- a sibling id like 'g1' is a substring of 'g10', so a substring test reports a teardown that never happened; (5) only drop the record and release the leases after the entry and branch are actually gone.\n\nMEASURED SHAPES (git 2.53.0, Linux, 2026-09-19): `git worktree remove --force <path>` SUCCEEDS when the directory was deleted by hand (no fallback needed); a plain `git worktree prune` with NO --expire drops the stale administrative entry IMMEDIATELY (the gc.worktreePruneExpire grace does not gate an explicit prune for a missing directory), so no --expire=now is required; `git branch -D` deletes a branch holding commits that exist nowhere else in the repo, and the base branch is untouched. A discard must also work while the repo root has a DIFFERENT branch checked out (a merge-back refuses that shape; an abandon must not).\n\nVERIFICATION: acceptance tests driven against REAL git in throwaway repos (worktree deleted by hand -> typed refusal with the session intact; --discard -> record/branch/worktree-entry/leases all gone, root HEAD unmoved; dirty session discarded; merge path and dirty refusal unchanged), plus a black-box probe on the built binary reproducing the original symptom verbatim.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "git-worktree-teardown-missing-directory", "provider": "openrouter", "solved_at": "2026-09-19T12:37:47.111Z", "version": ""}