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

@@ -14,7 +14,8 @@
# modules/ trace that makes a plain `git worktree remove` refuse) -> optional
# creator session registered in the control root's .mam -> summary with BASE=.
#
# Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule.
# Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule,
# 3 refused to protect existing content (nothing changed).
set -euo pipefail
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -98,6 +99,9 @@ else
fi
mkdir -p "$(dirname "$WT")"
if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$BRANCH"; then
if [ -n "$BASE_REF" ]; then
echo "WARNING: branch '$BRANCH' already exists; --base '$BASE_REF' is ignored (the worktree checks the branch out as it is)" >&2
fi
git -C "$MAIN" worktree add "$WT" "$BRANCH" || exit 1
else
git -C "$MAIN" worktree add -b "$BRANCH" "$WT" "${BASE_REF:-HEAD}" || exit 1
@@ -151,18 +155,34 @@ if [ -f "$WT/.gitmodules" ]; then
fi
done
done < <(git config --file "$WT/.gitmodules" --get-regexp '^submodule\..*\.path$' 2>/dev/null || true)
# A submodule path that is not initialised yet but already holds files
# (review R4): git refuses to clone into it, and a rollback could not tell
# those files from its own - refuse before changing anything. With this,
# everything under a not-yet-initialised path after a failed attempt was
# created by that attempt, which is what the rollback relies on.
blocked=()
for ((i = 0; i < ${#sm_names[@]}; i++)); do
p="$WT/${sm_paths[$i]}"
if [ "${sm_pre[$i]}" != "1" ] && [ -e "$p" ] && [ -n "$(ls -A "$p" 2>/dev/null)" ]; then
blocked+=("${sm_paths[$i]}")
fi
done
if [ "${#blocked[@]}" -gt 0 ]; then
echo "REFUSED: submodule path(s) not initialised yet but not empty: ${blocked[*]}. Initialising would fail on them and a rollback could not tell their files from its own; move the files away (or drop --submodules). Nothing was changed." >&2
exit 3
fi
if ! git -C "$WT" submodule update --init --recursive; then
# A submodule cloned by this attempt leaves a checkout whose .git file
# points into modules/<name>: deinit it before its git dir goes, or the
# worktree stops being a usable git checkout.
for i in $(seq 0 $(( ${#sm_names[@]} - 1 ))); do
for ((i = 0; i < ${#sm_names[@]}; i++)); do
if [ "${sm_pre[$i]}" = "1" ]; then continue; fi
git -C "$WT" submodule deinit -q -f -- "${sm_paths[$i]}" >/dev/null 2>&1 || true
if [ -f "$WT/${sm_paths[$i]}/.git" ]; then rm -f "$WT/${sm_paths[$i]}/.git"; fi
rm -rf "$gd/modules/${sm_names[$i]}"
done
if [ "$had_modules" != "1" ]; then rm -rf "$gd/modules"; fi
for i in $(seq 0 $(( ${#cfg_keys[@]} - 1 ))); do
for ((i = 0; i < ${#cfg_keys[@]}; i++)); do
if [ "${cfg_had[$i]}" = "1" ]; then
git -C "$MAIN" config "${cfg_keys[$i]}" "${cfg_vals[$i]}"
else