Refuse to reap a checkout with uncommitted work unless forced #5

Merged
forgejo-admin merged 1 commit from guard-reap-dirty-worktree into main 2026-07-31 20:35:55 +00:00

Found while reaping four real workspaces.

The hole

spawn reap guarded your commits but not your working tree. It passed herdr worktree remove --force unconditionally, so uncommitted and untracked files were discarded silently.

That's backwards. Unfinished work is precisely the work that hasn't been committed yet — the working tree deserved the stronger guard, not the weaker one.

Every one of the four workspaces I reaped happened to be clean, so nothing was lost. But that was luck. I'd been running git status by hand before each reap, which is exactly the check the tool should be doing itself.

The fix

Refuse a dirty checkout, list the offending paths (capped at 10), require explicit --force:

$ spawn reap w25
spawn: /Users/pbanka/.herdr/worktrees/herdrmastr/dirty-guard has 2 uncommitted change(s):
    M README.md
    ?? WIP.txt
  commit or stash them, or re-run with --force to discard
exit=1

Uses --untracked-files=all, so a brand-new file the agent wrote but never added still counts. Same shape as the existing branch guard: safe by default, destructive only when asked.

Verification

check result
dirty checkout (1 modified + 1 untracked), no --force refused, both listed, files left on disk
same checkout with --force removed as normal, branch deleted
spawn selftest / openspec validate --all ok / 4 passed

Spec gains two scenarios on the reap requirement.

Context

Reaped this session with the pre-fix version, all verified clean by hand first: w1G cpanel-rollout, w1J typechecking-modules, w18 access-control-lists-slice-1, w1F celilo-deployment-cleanup. Work on dict-2 (10 commits), e2e-audit (5), audit-fleet-rollout (2) and fix-prod-smoke-failures (1) was left untouched.

🤖 Generated with Claude Code

Found while reaping four real workspaces. ## The hole `spawn reap` guarded your **commits** but not your **working tree**. It passed `herdr worktree remove --force` unconditionally, so uncommitted and untracked files were discarded silently. That's backwards. Unfinished work is precisely the work that hasn't been committed yet — the working tree deserved the *stronger* guard, not the weaker one. Every one of the four workspaces I reaped happened to be clean, so nothing was lost. But that was luck. I'd been running `git status` by hand before each reap, which is exactly the check the tool should be doing itself. ## The fix Refuse a dirty checkout, list the offending paths (capped at 10), require explicit `--force`: ``` $ spawn reap w25 spawn: /Users/pbanka/.herdr/worktrees/herdrmastr/dirty-guard has 2 uncommitted change(s): M README.md ?? WIP.txt commit or stash them, or re-run with --force to discard exit=1 ``` Uses `--untracked-files=all`, so a brand-new file the agent wrote but never added still counts. Same shape as the existing branch guard: safe by default, destructive only when asked. ## Verification | check | result | |---|---| | dirty checkout (1 modified + 1 untracked), no `--force` | refused, **both listed**, files left on disk | | same checkout with `--force` | removed as normal, branch deleted | | `spawn selftest` / `openspec validate --all` | ok / 4 passed | Spec gains two scenarios on the reap requirement. ## Context Reaped this session with the pre-fix version, all verified clean by hand first: `w1G` cpanel-rollout, `w1J` typechecking-modules, `w18` access-control-lists-slice-1, `w1F` celilo-deployment-cleanup. Work on `dict-2` (10 commits), `e2e-audit` (5), `audit-fleet-rollout` (2) and `fix-prod-smoke-failures` (1) was left untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`spawn reap` guarded commits but not the working tree: it passed
`herdr worktree remove --force` unconditionally, so uncommitted and untracked
files were discarded without a word. That is backwards — unfinished work is
precisely the work that has not been committed yet, so the working tree needed
the stronger guard, not the weaker one.

Reap now refuses a dirty checkout, lists the offending paths (capped at 10),
and requires an explicit --force to discard. Matches the shape of the existing
branch guard: safe by default, destructive only when asked.

Found while reaping four real workspaces. Every one happened to be clean, so
nothing was lost — but that was luck, not design. I had been hand-checking
`git status` before each reap, which is exactly the check the tool should be
doing itself.

Verified: a worktree with one modified tracked file and one untracked file is
refused with both listed and the files left intact; --force removes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo-admin/herdrmastr!5
No description provided.