fix(issue-21): reviews 8f141362 N1 and 275fdc4f R4/R5 - clean refusal for a broken worktree, no init over existing files, check retained submodule repositories

- N1 (planner-reviewer-claude-01) worktree_remove.sh: when `git status` fails
  in the worktree (e.g. its directory was deleted), judge_tree returned before
  METHOD was set and the summary line died under set -u with exit 1. METHOD
  now has its default before the function; the refusal is exit 3 with
  "nothing was changed", plus a `git worktree prune` hint when the directory
  is gone. Same for --dry-run.
- R4 (planner-reviewer-codex-01) worktree_prepare.sh: a submodule path that is
  not initialised yet but already holds files made the clone fail, and the
  rollback's `deinit -f` then deleted those files. --submodules now refuses
  such a path up front (new exit 3, nothing changed), which also makes the
  rollback's premise true: after a failed attempt, everything under a
  not-yet-initialised path was created by that attempt.
- R5 (planner-reviewer-codex-01) lib_py/worktree.py: `submodule foreach`
  visits initialised checkouts only, but a deinit'd submodule keeps its
  repository - stash and unpushed commits included - under the worktree's
  modules/, which the forced removal deletes. Every repository under modules/
  (nested ones included) is now checked directly (`--git-dir`): commits on no
  remote (HEAD included) and refs/stash with its reflog. A directory that looks
  like a partial repository cannot be checked and refuses.
- Also: the rollback loops are C-style (`seq 0 -1` counts down on BSD), and
  prepare warns that --base is ignored for an existing branch.

Tests W6i (deleted worktree directory, with and without --dry-run), W6j and
W6j2 (stash / unpushed commit in a deinit'd submodule's repository) and W6k
(existing files in an uninitialised submodule path). Skill docs and the design
doc describe the new refusal and the wider repository check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-14 18:14:13 +00:00
1 parent bbbbb95ef6
commit c7c559b7aa
7 files changed
+168 -8

No files matched your search

@@ -11,7 +11,8 @@
# delegate job of a session --stop will stop is cancelled by stop itself);
# (4) clean tree, submodules included; (5) submodules: nothing modified and no
# commit that is on no remote (HEAD included - a submodule is on a detached
# HEAD) and no stash. (4) and (5) run again right before the removal, after
# HEAD) and no stash - in every repository under the worktree's modules/,
# initialised or not. (4) and (5) run again right before the removal, after
# --stop: a session that is being stopped can still write, commit or stash.
# Removal is plain, or - when the worktree holds submodule state, which
# git refuses to remove without it - a single --force justified by (4) and (5).
@@ -84,10 +85,14 @@ fi
# (4) clean, submodules included, and (5) submodules. A function: it runs now,
# before any change, and again right before the removal (review R1). It sets
# METHOD from what it saw and returns 1 on any finding.
METHOD="plain" # set before any early return below: the summary line reads it under set -u
judge_tree() {
local found=0 dirty sub trace key val
if ! dirty="$(git -C "$WT" status --porcelain --ignore-submodules=none 2>&1)"; then
refuse "git status failed in the worktree: $dirty"
if [ ! -d "$WT" ]; then
echo " the directory no longer exists; if that is intended, run: git -C '$MAIN' worktree prune" >&2
fi
return 1
fi
if [ -n "$dirty" ]; then