From cbfc4eb52b27cd1755618c77c905a186a63d27f8 Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Mon, 7 Sep 2026 22:18:13 +0100 Subject: [PATCH 1/2] fleet-worktree: `create` said "nothing to create" about the state `check` had just refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #502 C3. Reproduced end to end in a throwaway repo before anything was changed. ⛔ THE DEFECT. With all 8 roles provisioned and every one of them NODOCTRINE — a tree that exists, sits in the conventional location, and whose HEAD carries no `prompts/`: check -> exit 1 "⛔ 8 role(s) have a tree that does NOT carry the doctrine" "⇒ Re-create from a ref that carries them: … 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 `, spelled exactly as the message spells it, behaves identically: the guard is `n_missing -eq 0`, and a NODOCTRINE tree EXISTS, so nothing is missing. ★ 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 `git worktree remove` can destroy uncommitted work. So `create` now REFUSES loudly and prints the two commands that actually do it, per role, rather than silently doing nothing or silently doing something. PROVEN TWO-SIDED, in a sandbox repo built to the exact state: ⛔ 8 trees, all NODOCTRINE, 0 MISSING before exit 0 "nothing to create — no role is without a tree" after exit 1 names all 8 and prints `git worktree remove && … create ` ✅ the same 8 trees, doctrine PRESENT after exit 0 "nothing to create — no role is without a tree" ⇒ The branch fires on the state it is for and does not over-fire on the healthy one. Without that second pole a guard that refused everything would have passed the first. ⚠ The sandbox needed building twice. My first attempt had ONE tree present, so `n_missing` was 7 and `create` cheerfully built the other seven — the defect needs `n_missing == 0` to be visible at all, and a partial fixture hid it. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/fleet-worktree.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/fleet-worktree.sh b/scripts/fleet-worktree.sh index 11e7d89..1166408 100755 --- a/scripts/fleet-worktree.sh +++ b/scripts/fleet-worktree.sh @@ -176,6 +176,35 @@ 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. + if [ "$n_missing" -eq 0 ] && [ "$n_nodoctrine" -gt 0 ]; then + printf '⛔ nothing is MISSING, but %d role(s) carry a tree WITHOUT the doctrine:%s\n' \ + "$n_nodoctrine" "$nodoctrine_list" + printf ' `create` cannot fix this: 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 + printf ' git worktree remove %s/%s && %s create \n' \ + "$WT_DIR" "$r" "$0" + done + 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' \ From eb8d79b1979ff4b243969a3b7bf7dbf7b24425db Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Mon, 7 Sep 2026 22:53:52 +0100 Subject: [PATCH 2/2] fleet-worktree: the MIXED state left broken trees silently, and my repair line was unquoted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both review findings adopted. The first is the sharper, and it is the state my own reproduction had stumbled into and walked past. 1. ⛔ MIXED `MISSING` + `NODOCTRINE` EXITED 0 WITH BROKEN TREES LEFT BEHIND. My guard was `n_missing -eq 0 && n_nodoctrine -gt 0`, so it only fired when nothing was absent. With both counters non-zero, `create` provisioned the absent roles and said nothing about the existing doctrine-less ones. Isolated in a repo whose OLD commit lacks prompts/ and whose main carries them — 3 NODOCTRINE trees, 5 MISSING, valid ref: BEFORE rc=0 created 5 · nodoctrine reports 0 · 3 broken trees left, unmentioned AFTER rc=1 created 5 · names all 3 and prints the per-role repair ★ That mixed state is exactly the fixture my first reproduction built by accident — one tree present, seven missing — and it is why the original defect was invisible on the first attempt. I recorded that as "a partial fixture hid it" and did not notice the same partial state was also a hole in the fix. ⚠ MY FIRST BEFORE/AFTER WAS CONFOUNDED and I nearly published it. The fixture built its new trees from a ref with no prompts/, so the create loop set rc=1 for an unrelated reason and BEFORE read 1, not 0 — the finding says "returns success WHEN THE NEW TREES USE A VALID REF", and mine did not. Rebuilt with a valid ref, the isolation is exact. ⇒ The report is hoisted into `report_nodoctrine()` and called from BOTH entry points, because the state had two and the first version guarded one. 2. ⚠ THE PRINTED REPAIR COMMANDS WERE UNQUOTED. The operator COPIES those lines, and a worktree path containing a space — `C:\Program Files\…` is a normal location — yields a command that silently removes the wrong thing. `%q` now, and `--` before the path. git worktree remove -- /srv/Program\ Files/wt/architect ★ Same root as #502 C2, one surface along: there a space broke a PARSER, here it breaks a command a human is about to run. C2 was found by an installer, this by review, and neither sweep would have found the other. THREE STATES, all exercised: ⛔ 8 NODOCTRINE · 0 MISSING -> rc 1, report ⛔ 3 NODOCTRINE · 5 MISSING -> rc 1, report, 5 provisioned (was rc 0, silent) ✅ doctrine present -> rc 0, "nothing to create", no report Without the third, a branch that reported unconditionally would pass the first two. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/fleet-worktree.sh | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/scripts/fleet-worktree.sh b/scripts/fleet-worktree.sh index 1166408..e175728 100755 --- a/scripts/fleet-worktree.sh +++ b/scripts/fleet-worktree.sh @@ -193,16 +193,33 @@ create) # 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. - if [ "$n_missing" -eq 0 ] && [ "$n_nodoctrine" -gt 0 ]; then - printf '⛔ nothing is MISSING, but %d role(s) carry a tree WITHOUT the doctrine:%s\n' \ + # ⇒ 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 this: it only builds trees that are ABSENT, and these exist.\n' + 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 - printf ' git worktree remove %s/%s && %s create \n' \ - "$WT_DIR" "$r" "$0" + # ⚠ %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 @@ -249,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