diff --git a/scripts/fleet-worktree.sh b/scripts/fleet-worktree.sh index 11e7d89..e175728 100755 --- a/scripts/fleet-worktree.sh +++ b/scripts/fleet-worktree.sh @@ -176,6 +176,52 @@ check) ;; create) + # ⛔ NODOCTRINE IS NOT "NOTHING TO CREATE", and this branch said it was. Measured + # 2026-09-07 by reproducing the state in a throwaway repo (#502 C3): with 8 roles + # provisioned and every one of them NODOCTRINE, + # + # check -> exit 1, "⛔ 8 role(s) have a tree that does NOT carry the doctrine" + # "⇒ Re-create from a ref that carries them: fleet-worktree.sh create " + # create -> exit 0, "nothing to create — no role is without a tree" + # + # ⇒ THE TOOL TOLD THE OPERATOR TO RUN A COMMAND THAT REFUSES — and refuses with + # exit 0, reporting success for the state it had just called blocking. `create + # ` behaves identically, so following the message verbatim changes nothing. + # + # ★ THE FIX IS NOT TO AUTO-REMEDIATE. This file's own stance, six lines into + # `where()`, is that a suspect tree is "reported for a human to judge, and never + # auto-remediated" — and removing a worktree can destroy uncommitted work. So + # `create` REFUSES loudly and prints the two commands that actually do it, per + # role, rather than silently doing nothing or silently doing something. + # ⇒ HOISTED, because the NODOCTRINE state has TWO entry points and the first + # version only guarded one. When n_missing and n_nodoctrine are BOTH non-zero, + # `create` provisions the absent roles, exits 0 if the new trees are sound, and + # says nothing about the existing broken ones. ⛔ That mixed state is not + # hypothetical: it is the fixture my own reproduction hit by accident — one tree + # present, seven missing — and it is why the original defect stayed invisible on + # the first attempt. Found in review of this PR. + report_nodoctrine() { + printf '\n⛔ %d role(s) carry a tree WITHOUT the doctrine:%s\n' \ + "$n_nodoctrine" "$nodoctrine_list" + printf ' `create` cannot fix these: it only builds trees that are ABSENT, and these exist.\n' + printf ' Removing a worktree can destroy uncommitted work, so this is not done for you.\n' + printf ' ⇒ Per role, after checking the tree holds nothing you need:\n' + for r in $nodoctrine_list; do + # ⚠ %q, not %s. The operator COPIES these lines, and a worktree path + # containing a space — `C:\Program Files\...` is a normal location — would + # produce a command that silently removes the wrong thing. Same root as + # #502 C2, one surface along: there it broke a parser, here it breaks a + # command a human is about to run. + printf ' git worktree remove -- %q && %q create \n' \ + "$WT_DIR/$r" "$0" + done + } + + if [ "$n_missing" -eq 0 ] && [ "$n_nodoctrine" -gt 0 ]; then + printf '⛔ nothing is MISSING, but the doctrine is absent from some trees.\n' + report_nodoctrine + exit 1 + fi if [ "$n_missing" -eq 0 ]; then printf 'nothing to create — no role is without a tree\n' [ "$n_outside" -gt 0 ] && printf '⚠ but%s sit outside %s; MOVE those, creating would duplicate them\n' \ @@ -220,6 +266,13 @@ create) printf ' FAILED %-10s (path in use, or %s unreachable)\n' "$r" "$BASE_REF"; rc=1 fi done + # ⛔ THE MIXED STATE. Roles that were ABSENT have just been built; roles that + # already had a doctrine-less tree were never touched, and without this the run + # exits 0 having silently left them broken. + if [ "$n_nodoctrine" -gt 0 ]; then + report_nodoctrine + rc=1 + fi printf '\nnext, INSIDE your tree: cd %s/ || exit 1\n' "$WT_DIR" printf ' git checkout -b /\n' exit $rc