fix(wt): interim safety guards from #140 review follow-ups #141
No reviewers
Labels
No labels
blocked
bug
enhancement
high-priority
low-priority
needs-info
needs-triage
ready-for-agent
ready-for-human
referenced
research
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gabogg/hikcentral!141
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/wt-safety-interim"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The review follow-ups in #140 include two items that can cause harm today, plus one open design point that is causing friction:
wt rm .. -f/wt rm . -fpass the.worktrees/prefix check and reachgit worktree remove --forceon the main checkout. Only git's own refusal stops it.wt rm/wt prunefrom inside the worktree being removed deletes the calling session's working directory.data/hikcentral.dbandlogs/lifecycle.login every worktree, so the data-loss guard keeps nearly every worktree andwt rm -frisks becoming routine.Approach
This is an interim fix. It closes the two sharp edges and makes the guard's refusals informative, without changing what the guard protects.
holds_cwd():rmrefuses, andpruneskips with a message, when the caller's cwd is inside the target worktree.cmd_rmcanonicalises the target withrealpathand requires it to sit directly under.worktrees/.describe_local_files(): the "holds ignored local files" refusal now lists each file with its size, so a 4 KB empty DB is easy to tell from real data.workspaces.mdandforgejo-cli.mdsay to runrm/prunefrom the main checkout, and give the interim rule for-f. The permanent policy stays open in #140 (C2-3).Refs #140 (S2-3, C2-1; C2-3 interim only).
Verification
Exercised on local throwaway branch
docs/int-probe:rmwithdata/DB + log4096 bytes data/hikcentral.dband2 bytes logs/lifecycle.logrm .. -f/rm . -fnot a worktree directly under …/.worktreesrm -ffrom inside the worktreeis the current directory; run scripts/wt from the main checkoutprunefrom inside a prunable worktreeprunefrom the main checkoutPre-commit (ruff, pytest) is green and
scripts/check_docs.pyis clean.🤖 Generated with Claude Code
Code review: PR #141 (
fix/wt-safety-interim@a770766vsmaster@c096d72)Standards sources: AGENTS.md,
docs/standards/*,.claude/skills/writing-for-agents, and the Fowler smell baseline. Spec: the approved interim plan (sizes in the refusal with the guard unchanged; #140 S2-3; #140 C2-1). The C2-3 permanent policy stays in #140.Standards
Verified:
rm ..andrm .are refused, as is a subdirectory target and a cwd inside the target.holds_cwdhandles sibling names (foovsfoo-bar) and symlinked paths correctly. No breaches of AGENTS.md or the code standards.cmd_pruneruns the keep-guards before the merged/upstream-gone checks, so every live, unmerged worktree with adata/DB printskept …: check, then wt rm -f. That points agents atrm -fon other sessions' live work; it was observed on the #137/#138 worktrees. Move the guards after the candidate checks.describe_local_filesreceives C-quoted porcelain paths, so a top-level ignoredrun ñ.logwas silently missing from the list the docs tell you to read beforerm -f. Use--porcelain -zand read the entries null-delimited. An ignored directory also lists every file with no limit (judgement call).prunerunsholds_cwd/local_filesand then callsremove, which checks again. Use onekeep_reasonfor both.forgejo-cli.md"from the main checkout" repeatsworkspaces.md(single source of truth).workspaces.md"Run both from the main checkout" comes after three commands. Namermandprune.workspaces.md"list those files with their sizes" repeats the script's own output, and "~4 KB" is an environment fact that will go stale.usage()doesn't mention the cwd refusal.Spec
Verified in an isolated sandbox (bare origin + clone):
local_filesis identical, and the new path check is strictly tighter than before.rmandprune.rm .. -f,rm . -fand a.worktrees/evil -> mainsymlink are refused.rm(including with-f, from subdirectories, and via symlinked paths) and inprune.workspaces.mddeclares anylifecycle.log"safe to discard", which pre-decides part of option 1 in #140. Soften it to "usually", or drop the claim.Summary. Standards: 7 findings, worst S-1 (prune points to
rm -fon live unmerged worktrees). Spec: 2 findings, both P3; worst C-1 (incomplete size list for quoted names and symlinks). First pass, so all findings get fixed on this branch.🤖 Generated with Claude Code
Review fixes:
7944c7bEvery finding from the review is addressed; S-4 is deliberately kept, with the reason below.
prunenow runs the keep-guards only after a worktree is confirmed as a merged candidate (upstream gone + ancestor of master). Its messages read "kept …: merged, but …", and live unmerged worktrees are skipped silently. Verified: runningprunealongside the live #137/#138 worktrees prints nothing about them.local_filesusesgit -c core.quotePath=false status --porcelain -z --ignoredand is read NUL-delimited. The listing covers regular files and symlinks (find \( -type f -o -type l \)), falls back to the raw path whenfindgives nothing, and caps each entry at 20 files with "... and N more under X". Verified withrun ñ.log, a symlink indata/, and 23 files inlogs/.keep_reason()is used by bothrmandprune.holds_cwdstays separate, since-fmust never bypass it.forgejo-cli.md§Merging say "from the main checkout". An agent follows that doc at the moment it runsprune, right after merging from inside its PR worktree, so the clause sits where the mistake happens. The Spec requirement outranks the judgement call.rmandprunefrom the main checkout".usage()now ends with: "rm and prune never remove the worktree holding your cwd: run them from the main checkout."lifecycle.log, and says to treat anything else as real data until #140 settles the rule. It no longer declares logs safe.Verification: all cases were run on local throwaway branches with the branch's own script:
rmlisting: quoted name, symlink, the 20-file cap.pruneon a merged candidate: with files (kept), dirty (kept), clean (removed), and from inside it (kept, cwd).prunebeside live unmerged worktrees: silent.Pre-commit (ruff, pytest) is green and
check_docs.pyis clean.🤖 Generated with Claude Code