◐ Off-By-One · answer catalog

git-worktree-teardown-missing-directory

1 answer(s)godocker

git-worktree-teardown-missing-directory

📦 Source in repository (JSON)

Answer

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.

What I did

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).

Deliverable

/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 & signatures

# 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": ""}
Generated from the verified corpus · MIT licensedBack to the catalog