From 2da60cf663ded8c4f8cc2a3ef0cd69a729b47ca4 Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Mon, 7 Sep 2026 19:26:59 +0100 Subject: [PATCH 1/2] =?UTF-8?q?#502:=20route=20the=2014=20findings=20?= =?UTF-8?q?=E2=80=94=204=20already=20discharged,=205=20fixed=20here,=20eac?= =?UTF-8?q?h=20verified=20before=20acting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An outside installer vendored this fleet into a foreign repo on Windows 11 / Git Bash and filed 14 measured findings. Nobody had recorded which were still live. I checked every one against the current tree before touching anything, and the audit is half the value: ⛔ ALREADY FIXED, and the code says so by issue number — I nearly "repaired" three of these: A1 onboard.md's copy table names `.daintree/bootstrap.sh` and cites #502 A1/A2 A2 the same row states the commit-AND-PUSH precondition A3 step 1 now tests `cli-control.json` beside `cli.sock` A4 step 8 reads "9 of 10, measured 2026-09-06", not the stale "1 of 9" C5b check-orientation returns "void" on an unreadable README — the comment quotes this issue's own words. MEASURED: hiding tools/README.md gives exit 2, not a pass. FIXED HERE, each with a two-sided control: C1 · validate-recipe.py CRASHED on valid JSON that is not an object. `json.load` accepts any JSON value, so `[]` reached `recipe.get` and raised AttributeError — a validator whose purpose is refusing malformed input, crashing instead of reporting, so a caller cannot tell a rejected recipe from a broken validator. [] -> ERR "top-level JSON is list, not an object", exit 1 "a string" -> same · ✅ the real recipe still exits 0 C2 · fleet-worktree.sh TRUNCATED any worktree path containing a space. `--porcelain` emits `worktree ` with the path as the REST of the line; `awk '{print $2}'` takes the first word. Not hypothetical on Windows. `worktree /Users/x/Program Files/repo` old awk -> /Users/x/Program ⛔ silently wrong new sed -> /Users/x/Program Files/repo B3 · NO .gitattributes, so CRLF re-broke the scripts on every checkout. Committed blobs are LF, but `core.autocrlf=true` — the Git-for-Windows DEFAULT — makes the WORKING TREE CRLF, and bash executes the working tree: `fleet-preflight.sh: line 10: $'\r': command not found`. A one-time dos2unix does not hold; this pins it. git check-attr eol -> lf on .sh/.py/.json/.md · CONTROL: LICENSE -> unspecified B4 · THE CHECKERS CRASHED EXACTLY WHEN THEY FOUND SOMETHING. The ⛔/⚠ glyphs appear almost only on FAIL branches, and Windows Python defaults stdout to cp1252 — so a checker ran clean when all was well and died with UnicodeEncodeError when it detected a defect. A crashed checker reports nothing at all. Reproduced here under PYTHONIOENCODING=cp1252, three-way so the cause is not assumed: OLD + cp1252 -> exit 1, UnicodeEncodeError on '⚠' OLD + utf-8 -> exit 0 ⇒ the ENCODING, not the file NEW + cp1252 -> exit 0, zero errors errors="replace", not a hard switch: a mangled glyph is a legible finding, an exception is not. Applied to all five scripts/check-*.py. C4 · THREE PLACES CLAIMED "exit code is always 0" AND THE IMPLEMENTATION DISAGREED. Measured: forcing one FAIL gives preflight exit 1. ⇒ This is DOCUMENTATION ROT, not an open decision — the exit code was deliberately given a verdict, with a comment block explaining why, and two comments plus prompts/README.md were never updated. So the docs now state what the code does (0 clean · 1 blocking · 2 could not establish). ⚠ Whether the pane should GATE remains untouched and unasked. NOT ADDRESSED, and named rather than dropped: A3-adjacent Windows behaviour, B1/B2 (Daintree hardcodes PowerShell; `bash` resolves to the WSL launcher) — both need a Windows host to verify and neither is repairable here. C3, C6 and D (the vendoring path defeats the scope discriminator) are left for their own routing. Co-Authored-By: Claude Opus 5 (1M context) --- .gitattributes | 20 ++++++++++++++++++++ prompts/README.md | 2 +- scripts/check-goal-conformance.py | 10 ++++++++++ scripts/check-handoff-rows.py | 10 ++++++++++ scripts/check-onboard.py | 10 ++++++++++ scripts/check-orientation.py | 10 ++++++++++ scripts/check-tools-index.py | 10 ++++++++++ scripts/fleet-preflight.sh | 8 ++++++-- scripts/fleet-worktree.sh | 7 ++++++- scripts/validate-recipe.py | 10 ++++++++++ 10 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3f3f2c7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# ⛔ THE EXECUTABLE ARTIFACTS ARE LF, IN THE WORKING TREE, ON EVERY PLATFORM. +# +# Measured on a Windows 11 / Git Bash install (#502 B3): `scripts/fleet-preflight.sh: +# line 10: $'\r': command not found`. The committed blobs were already LF — but with +# `core.autocrlf=true`, which is the Git-for-Windows DEFAULT, the WORKING TREE is CRLF, +# and bash executes the working tree, not the blob. +# +# ⇒ Two consequences, and the second is why a one-time `dos2unix` is not the fix: +# · the estate's own clone is CRLF for the same reason, so a plain `cp` during +# onboard.md step 2 carries CR into the target repository +# · without this file the NEXT checkout re-breaks it, silently +# +# ⚠ This pins the working tree, not just the blob. `text eol=lf` is what does that; +# `-text` (binary) would also stop conversion but would break diffs. +*.sh text eol=lf +*.py text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/prompts/README.md b/prompts/README.md index df69961..08baf25 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -435,7 +435,7 @@ prints the expected roster as a checklist. It exists because **nine agents asserting they are ready is not the same as nine agents being ready**. The preflight establishes by execution what the agent panes each claim in prose, so the -claims have something to be checked against. It reports and never gates: exit code is always 0. +claims have something to be checked against. It does not gate the fleet — no pane waits on it — but **its exit code carries a verdict**: `0` clean · `1` blocking failures · `2` it could not establish its own. ⚠ This sentence read *"exit code is always 0"* for as long after the change as it took an outside installer to notice (#502 C4); `onboard.md` makes this the acceptance test for an install, and an acceptance test that cannot fail is worse than none. ### What this recipe cannot do diff --git a/scripts/check-goal-conformance.py b/scripts/check-goal-conformance.py index 007afef..7616471 100755 --- a/scripts/check-goal-conformance.py +++ b/scripts/check-goal-conformance.py @@ -72,6 +72,16 @@ """ import json, os, re, subprocess, sys +# ⛔ THESE SCRIPTS CRASH EXACTLY WHEN THEY FIND SOMETHING. Windows Python defaults stdout +# to cp1252, and the ⛔/⚠/★ glyphs appear almost only on FAIL branches — so a checker runs +# clean when all is well and dies with UnicodeEncodeError when it detects a defect, and a +# crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). +# ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an +# exception is not. +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + # The six, from goals/README.md "What a role goal must contain". Each entry is # (label, regex over headings). ⚠ Matched on HEADINGS, not on body text: a file # that merely discusses "reserved actions" in prose has not stated them where every diff --git a/scripts/check-handoff-rows.py b/scripts/check-handoff-rows.py index db95862..cef63fe 100755 --- a/scripts/check-handoff-rows.py +++ b/scripts/check-handoff-rows.py @@ -61,6 +61,16 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tools")) from runmarker import guard, result # noqa: E402 +# ⛔ THESE SCRIPTS CRASH EXACTLY WHEN THEY FIND SOMETHING. Windows Python defaults stdout +# to cp1252, and the ⛔/⚠/★ glyphs appear almost only on FAIL branches — so a checker runs +# clean when all is well and dies with UnicodeEncodeError when it detects a defect, and a +# crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). +# ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an +# exception is not. +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + HANDOFF = "docs/HANDOFF.md" # ⇒ The command starts at the first shell head. This is what makes the row splittable diff --git a/scripts/check-onboard.py b/scripts/check-onboard.py index e55f823..5bee9e3 100755 --- a/scripts/check-onboard.py +++ b/scripts/check-onboard.py @@ -29,6 +29,16 @@ import sys from pathlib import Path +# ⛔ THESE SCRIPTS CRASH EXACTLY WHEN THEY FIND SOMETHING. Windows Python defaults stdout +# to cp1252, and the ⛔/⚠/★ glyphs appear almost only on FAIL branches — so a checker runs +# clean when all is well and dies with UnicodeEncodeError when it detects a defect, and a +# crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). +# ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an +# exception is not. +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + ROOT = Path(__file__).resolve().parent.parent DOC = ROOT / "onboard.md" RECIPE = ROOT / ".daintree" / "recipes" / "nforma-fleet.json" diff --git a/scripts/check-orientation.py b/scripts/check-orientation.py index bb130e7..35cb76c 100755 --- a/scripts/check-orientation.py +++ b/scripts/check-orientation.py @@ -26,6 +26,16 @@ import sys from pathlib import Path +# ⛔ THESE SCRIPTS CRASH EXACTLY WHEN THEY FIND SOMETHING. Windows Python defaults stdout +# to cp1252, and the ⛔/⚠/★ glyphs appear almost only on FAIL branches — so a checker runs +# clean when all is well and dies with UnicodeEncodeError when it detects a defect, and a +# crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). +# ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an +# exception is not. +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + ROOT = Path(__file__).resolve().parent.parent DOC = ROOT / "CLAUDE.md" diff --git a/scripts/check-tools-index.py b/scripts/check-tools-index.py index f95f1a6..0b7449d 100755 --- a/scripts/check-tools-index.py +++ b/scripts/check-tools-index.py @@ -72,6 +72,16 @@ import sys from pathlib import Path +# ⛔ THESE SCRIPTS CRASH EXACTLY WHEN THEY FIND SOMETHING. Windows Python defaults stdout +# to cp1252, and the ⛔/⚠/★ glyphs appear almost only on FAIL branches — so a checker runs +# clean when all is well and dies with UnicodeEncodeError when it detects a defect, and a +# crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). +# ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an +# exception is not. +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + # ⇒ DEV5 wrote this block for #348; DEVOPS owns the file and this is the import site # offered for review. The shared predicate lives in tools/ so the two guards cannot # disagree about the same file — one module, referenced, never copied. diff --git a/scripts/fleet-preflight.sh b/scripts/fleet-preflight.sh index c535cf8..ff3e5d5 100755 --- a/scripts/fleet-preflight.sh +++ b/scripts/fleet-preflight.sh @@ -6,7 +6,9 @@ # panes will each *claim* in their ROLE-READY line — so those claims have # something to be checked against. # -# Exit code is always 0: this pane reports, it does not gate. +# ⛔ EXIT CODE CARRIES THE VERDICT: 0 clean · 1 blocking failures · 2 the script could +# not establish its own verdict. It said "always 0" here for as long as that was true and +# for a while after it stopped being (#502 C4) — see the reasoning at the Summary section. ROLES=(TEAMLEAD ARCHITECT DEVOPS DX DEV1 DEV2 DEV3 DEV4 DEV5) @@ -233,7 +235,9 @@ section 'Repository self-checks' # them — and that one was a fixture whose header says "Not run; scanned." A set of # instruments none of which is ever called is a citation network, not a toolchain. # These two are cheap, deterministic, and answer questions no reviewer reliably -# answers by eye. ⚠ This pane still does not gate: exit code is always 0. +# answers by eye. ⚠ This pane does not gate the FLEET, but it DOES carry a verdict in its +# exit code (0/1/2) — a caller may branch on it. #502 C4 found this line and two others +# still claiming "always 0" after the behaviour was deliberately changed. # ⇒ check-handoff-rows.py joins this loop because THE DEFECT IT CATCHES SHIPPED # (#637): a snapshot row named `--by-state`, a flag that prints no totals, so three # correct numbers sat under a command that cannot produce any of them. A review bot diff --git a/scripts/fleet-worktree.sh b/scripts/fleet-worktree.sh index c835cd9..8d9d9cd 100755 --- a/scripts/fleet-worktree.sh +++ b/scripts/fleet-worktree.sh @@ -25,7 +25,12 @@ set -u ROLES="architect devops dx dev1 dev2 dev3 dev4 dev5" -main_tree=$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}') +# ⛔ `awk '{print $2}'` TRUNCATES AT THE FIRST SPACE. `--porcelain` emits `worktree ` +# with the path as the REST of the line, not as field 2 — so `C:\Program Files\repo` +# becomes `C:\Program`, silently, and every path derived from it is wrong. Not +# hypothetical on Windows, where that directory is a normal location (#502 C2). +# ⇒ `sed` strips the known prefix and keeps everything after it. +main_tree=$(git worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p') [ -n "$main_tree" ] || { echo "not inside a git repository" >&2; exit 2; } WT_DIR="$main_tree/.claude/worktrees" diff --git a/scripts/validate-recipe.py b/scripts/validate-recipe.py index 566b847..036ce6b 100755 --- a/scripts/validate-recipe.py +++ b/scripts/validate-recipe.py @@ -45,6 +45,16 @@ def validate(path): except (OSError, json.JSONDecodeError) as exc: return [f"{path}: unreadable or invalid JSON: {exc}"], [], None + # ⛔ VALID JSON IS NOT AN OBJECT. `json.load` accepts any JSON *value* — a top-level + # list, string, number or null all parse cleanly and then reach `recipe.get`, which + # raises AttributeError. Measured 2026-09-07 on `[]`: traceback, exit 1, no report. + # ⇒ For a validator whose entire purpose is REFUSING malformed input, crashing + # instead of reporting is the wrong failure mode — the caller cannot tell a rejected + # recipe from a broken validator. (#502 C1, raised by an external reviewer.) + if not isinstance(recipe, dict): + return [f"{path}: top-level JSON is {type(recipe).__name__}, not an object — " + f"a recipe must be a JSON object"], [], None + for field in ("id", "name"): if not isinstance(recipe.get(field), str) or not recipe[field]: errs.append(f"{field} must be a non-empty string") From 094a0c9e2ce5f67400bdb28c904c109b776557ff Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Mon, 7 Sep 2026 19:35:25 +0100 Subject: [PATCH 2/2] =?UTF-8?q?#502:=20both=20review=20findings=20adopted?= =?UTF-8?q?=20=E2=80=94=20and=20one=20of=20them=20had=20a=20THIRD=20site?= =?UTF-8?q?=20I=20found=20by=20sweeping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. PER-STREAM GUARDS. I wrote `if hasattr(sys.stdout, "reconfigure")` and then reconfigured BOTH streams inside it. `hasattr` on stdout says nothing about stderr — a harness may replace one and not the other, and this repo's own stubbed suites capture streams — so the guard meant to prevent an AttributeError could raise one. Demonstrated both ways against a stderr with no `.reconfigure`: buggy guarded form -> ATTRIBUTEERROR 'Fake' object has no attribute 'reconfigure' per-stream form -> EXIT 0 Applied to all five scripts/check-*.py. 2. ⛔ THE SPACE-TRUNCATION FIX WAS INCOMPLETE, AND THE REVIEW FOUND ONE MORE SITE THAN I HAD. `scripts/fleet-preflight.sh:36` carried the same `awk '{print $2}'` over the same porcelain output. ⇒ So I stopped fixing FILES and swept for the PATTERN — which turned up a THIRD site the review did not name: `fleet-worktree.sh:59`, inside `where()`, doing `p = $2` on the same input. All three now take the rest of the line: worktree /srv/Program Files/repo sed -> /srv/Program Files/repo substr -> /srv/Program Files/repo ⛔ $2 -> /srv/Program ⛔ CONTROL: zero field-2 porcelain parses survive anywhere under scripts/. ★ THE LESSON IS THE SWEEP, NOT THE PATCH. I had fixed the instance named in the issue and called the finding routed. It was a CLASS with three members, and the third would have survived both the original report and the review that caught the second. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/check-goal-conformance.py | 10 +++++++--- scripts/check-handoff-rows.py | 10 +++++++--- scripts/check-onboard.py | 10 +++++++--- scripts/check-orientation.py | 10 +++++++--- scripts/check-tools-index.py | 10 +++++++--- scripts/fleet-preflight.sh | 5 ++++- scripts/fleet-worktree.sh | 3 ++- 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/scripts/check-goal-conformance.py b/scripts/check-goal-conformance.py index 7616471..95e8be8 100755 --- a/scripts/check-goal-conformance.py +++ b/scripts/check-goal-conformance.py @@ -78,9 +78,13 @@ # crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). # ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an # exception is not. -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") +# ⚠ EACH STREAM GUARDED SEPARATELY. `hasattr(sys.stdout, ...)` says nothing about +# sys.stderr — a harness may replace one and not the other (this repo's own stubbed +# suites capture streams), and the guarded form would then raise AttributeError from +# inside the guard meant to prevent one. +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8", errors="replace") # The six, from goals/README.md "What a role goal must contain". Each entry is # (label, regex over headings). ⚠ Matched on HEADINGS, not on body text: a file diff --git a/scripts/check-handoff-rows.py b/scripts/check-handoff-rows.py index cef63fe..3cdf39e 100755 --- a/scripts/check-handoff-rows.py +++ b/scripts/check-handoff-rows.py @@ -67,9 +67,13 @@ # crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). # ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an # exception is not. -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") +# ⚠ EACH STREAM GUARDED SEPARATELY. `hasattr(sys.stdout, ...)` says nothing about +# sys.stderr — a harness may replace one and not the other (this repo's own stubbed +# suites capture streams), and the guarded form would then raise AttributeError from +# inside the guard meant to prevent one. +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8", errors="replace") HANDOFF = "docs/HANDOFF.md" diff --git a/scripts/check-onboard.py b/scripts/check-onboard.py index 5bee9e3..dd86908 100755 --- a/scripts/check-onboard.py +++ b/scripts/check-onboard.py @@ -35,9 +35,13 @@ # crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). # ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an # exception is not. -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") +# ⚠ EACH STREAM GUARDED SEPARATELY. `hasattr(sys.stdout, ...)` says nothing about +# sys.stderr — a harness may replace one and not the other (this repo's own stubbed +# suites capture streams), and the guarded form would then raise AttributeError from +# inside the guard meant to prevent one. +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8", errors="replace") ROOT = Path(__file__).resolve().parent.parent DOC = ROOT / "onboard.md" diff --git a/scripts/check-orientation.py b/scripts/check-orientation.py index 35cb76c..0993606 100755 --- a/scripts/check-orientation.py +++ b/scripts/check-orientation.py @@ -32,9 +32,13 @@ # crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). # ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an # exception is not. -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") +# ⚠ EACH STREAM GUARDED SEPARATELY. `hasattr(sys.stdout, ...)` says nothing about +# sys.stderr — a harness may replace one and not the other (this repo's own stubbed +# suites capture streams), and the guarded form would then raise AttributeError from +# inside the guard meant to prevent one. +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8", errors="replace") ROOT = Path(__file__).resolve().parent.parent DOC = ROOT / "CLAUDE.md" diff --git a/scripts/check-tools-index.py b/scripts/check-tools-index.py index 0b7449d..b656ea8 100755 --- a/scripts/check-tools-index.py +++ b/scripts/check-tools-index.py @@ -78,9 +78,13 @@ # crashed checker reports nothing at all (#502 B4, measured on a Windows 11 install). # ⇒ errors="replace" rather than a hard switch: a mangled glyph is a legible finding, an # exception is not. -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") +# ⚠ EACH STREAM GUARDED SEPARATELY. `hasattr(sys.stdout, ...)` says nothing about +# sys.stderr — a harness may replace one and not the other (this repo's own stubbed +# suites capture streams), and the guarded form would then raise AttributeError from +# inside the guard meant to prevent one. +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8", errors="replace") # ⇒ DEV5 wrote this block for #348; DEVOPS owns the file and this is the import site # offered for review. The shared predicate lives in tools/ so the two guards cannot diff --git a/scripts/fleet-preflight.sh b/scripts/fleet-preflight.sh index ff3e5d5..1ad4a4f 100755 --- a/scripts/fleet-preflight.sh +++ b/scripts/fleet-preflight.sh @@ -33,7 +33,10 @@ else # ⛔ NOT basename "$toplevel" — in a worktree that is the worktree's directory # name ("devops"), not the repository ("nForma-NEXT"). The main tree is always # the first entry of `git worktree list --porcelain`. - main_tree=$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}') + # ⛔ NOT `awk '{print $2}'` — `--porcelain` puts the path in the REST of the line, so + # field 2 truncates at the first space (#502 C2). Fixed in fleet-worktree.sh first; + # this second site was found by sweeping for the pattern rather than the file. + main_tree=$(git worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p') repo=$(basename "${main_tree:-$toplevel}") branch=$(git branch --show-current 2>/dev/null) ok "repo=$repo branch=${branch:-} — THIS TREE ONLY" diff --git a/scripts/fleet-worktree.sh b/scripts/fleet-worktree.sh index 8d9d9cd..11e7d89 100755 --- a/scripts/fleet-worktree.sh +++ b/scripts/fleet-worktree.sh @@ -57,7 +57,8 @@ WT_DIR="$main_tree/.claude/worktrees" # MISSING no tree at all -> create it where() { git worktree list --porcelain | awk -v want="$WT_DIR/$1" -v role="$1" ' - /^worktree /{ p = $2 + # ⛔ substr, NOT $2 — same truncation as the main_tree parse above, third site. + /^worktree /{ p = substr($0, 10) if (p == want) { found = 1; next } # HEURISTIC, and it is one: a path whose last element contains the role # token is PROBABLY that tree. Nothing binds a worktree to a role, so this