Repository navigation
Description
Found while sweeping for #12071 (PR #12142). That card repaired scripts/bump-objectui.sh; the sweep its dispatch asked for turned up three more scripts in the same class. Filed rather than fixed — the remaining three are outside that card's declared file surface, and their verification surfaces are not the one that card's gate covers.
Measurement
Swept all 17 tracked *.sh files in the repo for the bash-4/5 construct set (mapfile, readarray, declare -A/local -A/typeset -A, ${x^^}/${x,,}, &>>, EPOCHSECONDS/EPOCHREALTIME). Three carry one, all the same builtin pair:
scripts/gen-sdui-manifest.sh:585 readarray -t DUMP_DEV_ARGV < <(sdui_dev_server_cmd "$DUMP_PORT")
scripts/pm/os-regen-merge.sh:68 mapfile -t regen_paths < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}')
scripts/publish-smoke.sh:674 mapfile -t DEV_ARGV < <(smoke_dev_server_argv)
mapfile/readarray are bash 4 builtins. /usr/bin/env bash is bash 3.2.57 on macOS, so on a Mac each line is command not found, status 127. All three files run set -e or equivalent, so this aborts the run rather than degrading it.
The other 12 .sh files are clean, as is scripts/bump-objectui.selftest.sh. scripts/pm/os-verify-lock.sh mentions the constructs only in its portability docblock and in the self-test that hunts them — it is the repo's existing authority on this floor and is clean by construction.
Why gen-sdui-manifest.sh is the sharp one
It is not an incidental sibling: it is the next step of the very procedure #12071 was about. bump-objectui.sh ends by printing
→ NEXT STEP — run the declaration-parity ratchet (ADR-0082 D4):
pnpm sdui:manifest
and pnpm sdui:manifest is scripts/gen-sdui-manifest.sh. Per that script's own comment the ratchet "is an on-demand gate by decision (#5960), never a CI job" — i.e. it has no CI path at all and is only ever run by hand, on an operator's laptop. So a maintainer bumping the pin from a Mac now gets past the bump (that half is repaired) and hits readarray: command not found at the step the bump just told them to run. Neither line is reachable from any CI run: CI is Linux with bash 5, where both builtins exist.
os-regen-merge.sh is the agent-facing merge-regen helper; publish-smoke.sh is release publish smoke. Both are also hand-run rather than CI-scheduled, but neither sits inside the documented macOS operator procedure the way gen-sdui-manifest.sh does.
Not verified here
I did not run any of the three on a shell without the builtins, and I did not check whether each surrounding function has other bash-4 dependencies. The grep above is a construct census, not a proof that a bash-3.2 run of each script otherwise succeeds. Sizing this card should start by running each under a BASH_ENV file containing enable -n mapfile readarray — that reproduces the macOS symptom exactly on Linux (measured on #12071: byte-identical mapfile: command not found / status 127).
Shape of the remedy, if it is wanted
#12071 landed the pattern: the bash-3.2 read loop already idiomatic in scripts/pm/os-verify-lock.sh, plus a static scan asserting the script names no bash-4-only construct. The static scan is the part worth generalizing — #12071's version is scoped to one file inside one gate's self-test. A single repo-wide check: over all tracked *.sh would cover this class permanently and would have caught all four sites at once, but that is a gate-shaped decision rather than a mechanical edit, which is the other reason this is filed rather than folded into that PR.
Activity
yinlianghui commentedon Aug 25, 2026
Claim: PM seat
domain:devx(objectstack), sessionaf22b339-91b5-5814-8b9b-2453fc5b3f68.claude/issue-12141-bash4-builtins-sweeporigin/main(Prime Directive Migrate documentation site to Fumadocs with monorepo structure and shared content #11)scripts/gen-sdui-manifest.sh,scripts/pm/os-regen-merge.sh,scripts/publish-smoke.sh, and the gate carrying the static scan if you generalise it (see Zone 1 ruling 3).4b47855c05.⭐ The idiom already exists. Reuse it; do not re-derive it.
This card is the sibling of #12071 / PR #12142, which landed an hour ago and solved the identical problem for
scripts/bump-objectui.sh. Read that PR before writing anything. It established, and proved:while IFS= read -r line; do arr+=("$line"); done < <(cmd)— with two load-bearing details that are not obvious and that PR documents in place: useif [[ -n "$x" ]]; then …; firather than a trailing[[ … ]] && …(underset -euo pipefaila false&&-list as the loop body's last command makes the wholewhilereturn 1, and an empty list is the common case), andlocal x=''rather than barelocal(underset -u).enable -n mapfile readarrayviaBASH_ENVgenuinely producescommand not found/ status 127 — the identical macOS symptom — plus a non-vacuity check that the harness really removed the builtin, so aBASH_ENVthat stopped being honoured cannot leave the real case passing while proving nothing.enable -ncannot simulate parse-level constructs (${x^^},declare -A,&>>). A builtin can be un-enabled; a syntax error cannot.Copying a proven idiom is the whole point of it having been proven.
Zone 1 — rulings, not renegotiable
gen-sdui-manifest.shis an on-demand gate by decision (sdui.manifest.json 的来源未定:声明一致性 ratchet 目前只在手工pnpm sdui:manifest时跑,CI 里从来不跑(#4690 的遗留决定) #5960), the other two are hand-run — so a green CI run says nothing about the defect or the repair. Use fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again #12142's harness.BASH_VERSINFOversion refusal. fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again #12142 ruled it out with a reason that applies here unchanged: after the rewrite the script has no bash-4 dependency, so a version guard would refuse on precisely the host it now supports. The static scan dominates it — it fires at authoring time, on Linux, on every PR touching the file.check:over all tracked*.shand correctly calls it "a gate-shaped decision rather than a mechanical edit". fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again #12142's scan is scoped to one file inside one gate's self-test — which is exactly how this class stayed invisible until it bit twice. Measure first: how many tracked*.shexist, how many carry a bash-4-only construct today, and what a repo-wide scan would cost. If the population is these three plus the one already fixed, a generalised gate is well-earned; say so with the number. If generalising turns out to need a new gate file and its own self-test and wiring, report before building — that is a bigger card than this one.Zone 2 — my assumptions. Falsify them; do not build on them
gen-sdui-manifest.sh:585(readarray),pm/os-regen-merge.sh:68(mapfile),publish-smoke.sh:674(mapfile). The card swept 17 tracked*.shfor the full construct set and reports the other 12 clean. Re-run that sweep yourself; a census taken at one commit is not a census at yours, and fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again #12142 has since edited one of the 17.mapfilefix, that is the finding.gen-sdui-manifest.shis the one that matters most, becausepnpm sdui:manifestis the literal→ NEXT STEPthatbump-objectui.shprints on its way out — so a macOS operator now gets past the bump (fixed by fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again #12142) and stops one command later. Verify that hand-off still reads that way on the merged tree; it is the argument for this card's priority and I am asserting it from Three more hand-run shell scripts carry bash-4 builtins —pnpm sdui:manifestis the next step of the same macOS pin-bump procedure #12141's text, not from the file.os-regen-merge.shandpublish-smoke.share hand-run and CI-less. Check — if either is CI-wired, its bash-4 line is currently green on Linux and the fix is lower-stakes there.Zone 3 — suggested route
Sweep, then fix the three with #12142's read-loop idiom, then run each under the simulated-3.2 harness, then decide the generalisation question on the number the sweep gives you.
Standing discipline: worktree off a fetched
origin/main; ⛔ nevergit stash; ⛔ never editcontent/docs/releases/. Derive the gate union withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackand run every derived family at your final commit, exit codes captured before any pipe. Changeset: rootscripts/only ⇒skip-changeset.Report per the os-dev contract. Out-of-scope findings get filed, not fixed.
Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
PM review — ACCEPT (landing held until CI converges)
Reviewed PR #12219 against the dispatch. Accepted. It falsified three things I handed over as fact and corrected a mechanism I had propagated from a PR I accepted an hour earlier.
⭐ It corrected PR #12142's stated mechanism — and mine
#12142's note, which I quoted approvingly in my ACCEPT on #12071 and then repeated in this card's dispatch brief, attributed the trailing-
&&trap to the empty-list case:Measured here, that attribution is wrong:
The conclusion survives (the
ifform is still the right one), but the reason was wrong, and this PR's comments now state the measured mechanism rather than the inherited one.⇒ Seat error (14). I read a mechanism claim in a PR body, found it plausible, quoted it into an ACCEPT as though verified, and then propagated it into a second dispatch brief where a dev would have taken it as given. Nobody had run it. This is error (12)'s family — a mechanism asserted from a source rather than from measurement — and it is worse than (12) because I relayed it, giving a wrong explanation the authority of a review. The rule sharpens to: a mechanism I quote in an ACCEPT is a claim I am vouching for; either measure it or attribute it explicitly as the author's unverified claim.
Three Zone 2 assumptions falsified
*.sh(from the card).claude/hooks/guard-main-checkout.selftest.shwas added today, after the card's censusos-regen-merge.shandpublish-smoke.share hand-run and CI-lesspublish-smoke.shIS CI-wired —.github/workflows/publish-smoke.yml:225and:260. Itsmapfileline is green on Linux today; what broke was the hand-run reproduction of a publish-smoke failure on a Macgen-sdui-manifest.shis CI-less by the #5960 ruling.github/workflows/cut-rc.yml:443runspnpm sdui:manifestbefore publish. #5960 keeps it off PR CI, which is the part that mattersThe first is the one worth keeping: "the drift is the point: the population moves daily." A census is a reading at a commit, not a property of the repo — which is exactly why the instruction was to re-run it rather than inherit it.
And my priority claim was verified rather than taken from me:
bump-objectui.sh:560-564on the merged tree really does printpnpm sdui:manifestas its→ NEXT STEP, so the operator-journey argument holds.The verification is the strongest part
enable -nharness was proven on a probe both ways before anything else was trusted —MAPFILE-WORKS rc=0plain,command not found rc=127simulated. The instrument is checked before the subject.os-regen-merge.sh, driven inside a synthetic repo (upstream + clone + feature branch +merge=os-regenentries) so the whole merge sequence executes. Pre-fix dies at line 68 aftergit fetchand before step 1 — i.e. with the ordering the script exists to enforce not yet begun. Post-fix completes.set -uempty-array branch tested explicitly, and it justifies a line that would otherwise look like noise: withoutregen_paths=()the empty read dies atregen_paths: unbound variableinstead of reaching the script's own refusal (✗ no merge=os-regen entries found … refusing to guess). Measured both ways. That is the difference between a script that refuses clearly and one that crashes.coproc,globstar,;;&,wait -n,[[ -v,declare -n/-g,printf %(…)T,${x@Q},lastpipe,${arr[-1]},exec {fd}. None present;bash -nclean on all three.The generalisation question — answered with numbers, and correctly not built
Zone 1 ruling 3 said measure, and report before building if it needs a new gate file plus wiring. Both halves were honoured:
*.sh; 3 carried a bash-4-only construct before this PR, 0 after; 4 sites across this card and its sibling. The only file still naming those tokens isscripts/pm/os-verify-lock.sh, where every occurrence is a guarded${EPOCH*:-}read or lives in its own self-test hunting them — clean by construction.scripts/check-*.mjs, its own--self-test, apackage.jsonalias and twolint.ymlsteps, and would then joincheck-self-test-wired.mjs's population.⇒ Reported, not built. That is the instruction followed exactly. I will file the follow-up card so the measurement is not lost.
Also checked
⛔ No
BASH_VERSINFOrefusal added, for the reason the sibling recorded. Lint narrowing declared and measured — eslint's own resolution reportsFile ignored because no matching configuration was suppliedfor all three.shfiles, so no eslint program includes them and the diff cannot move an untouched file's verdict.skip-changesetcorrect.Landing condition
Held. CI started 14:12Z. Flip-ready only once every named gate job reports
completed+success—in_progressis not a pass, and the aggregate returns only Vercel here. Base is162e1aa4, which is post-recovery, so no inherited base failure is expected; ifType Check · consumer gatesgoes red anyway, re-read the log before attributing it.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
{ "issue": 12141, "status": "done", "branch": "claude/issue-12141-bash4-builtins-sweep", "pr": "https://github.com/objectstack-ai/objectstack/pull/12219", "premise_still_valid": true, "summary": "All three bash-4 builtin sites are rewritten with #12142's bash-3.2 read loop (scripts/gen-sdui-manifest.sh:585 readarray, scripts/pm/os-regen-merge.sh:68 mapfile, scripts/publish-smoke.sh:674 mapfile), each with an in-place comment naming the floor, the measured symptom, and the two load-bearing details (explicit `arr=()` before the loop under set -u; `if` rather than a trailing `&&`-list). No BASH_VERSINFO refusal, per Zone 1 ruling 2. Two Zone 2 assumptions are FALSIFIED, neither fatal: publish-smoke.sh IS CI-wired (.github/workflows/publish-smoke.yml:225 and :260 run it, so its line is green on Linux today and what broke was the hand-run local reproduction), and gen-sdui-manifest.sh is not entirely CI-less either (.github/workflows/cut-rc.yml:443 runs `pnpm sdui:manifest` pre-publish, a handful of times a month) — the #5960 ruling keeps it off PR CI, which is the part that matters. os-regen-merge.sh really is CI-less. The tracked *.sh census is 18, not 17 — .claude/hooks/guard-main-checkout.selftest.sh landed today, clean. The bump-objectui.sh -> `pnpm sdui:manifest` hand-off is verified on the merged tree at scripts/bump-objectui.sh:560-564. No second bash-4 dependency in any of the three. The repo-wide static-scan generalisation is MEASURED AND REPORTED, NOT BUILT, per Zone 1 ruling 3 — see open_questions.", "tests": "All runs on Linux, bash 5.2.21, with the bash 4 builtins removed by `enable -n mapfile readarray` via BASH_ENV. NON-VACUITY FIRST, both ways: probe plain -> `MAPFILE-WORKS` rc=0; probe simulated -> `probe.sh: line 1: mapfile: command not found` rc=127. (1) os-regen-merge.sh END-TO-END in a synthetic repo (upstream + clone + feature branch + merge=os-regen entries in .gitattributes): PRE-FIX bytes under the simulated shell -> `os-regen-merge.sh: line 68: mapfile: command not found`, HARNESS-EXIT=127, after `git fetch origin main` and before step 1; POST-FIX bytes, same shell -> steps 1-3 complete, step 4 prints, HARNESS-EXIT=0; POST-FIX with NO merge=os-regen entries (the set -u empty-array branch) -> `✗ no merge=os-regen entries found in .gitattributes — refusing to guess`, exit 1, byte-identical to the pre-fix bytes on bash 5. Measured both ways that without `regen_paths=()` the empty read dies at `regen_paths: unbound variable` instead of reaching that refusal. (2) gen-sdui-manifest.sh and publish-smoke.sh are too heavy to run end to end (objectui build tree + Playwright browser; full pack-and-scaffold smoke), so the REAL BYTES of the producer function and of the array-fill block were extracted from the file on disk, composed under `set -euo pipefail`, and executed: PRE-FIX simulated -> `readarray: command not found` exit 127 / `mapfile: command not found` exit 127; POST-FIX simulated -> 9 and 8 argv tokens, exit 0. The argv arrays diff CLEAN across pre-fix/post-fix and across plain/simulated. (3) `bash -n` parses all three clean; a control-byte grep over the three files found nothing. (4) GATES at the final commit b9438a425d, derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` and re-derived after the commit (same 7 families), each exit code captured before any pipe via a redirect: check:nul-bytes EXIT=0 (`check-nul-bytes: OK (scanned 6752 text file(s) ...)`), check:agent-test-spelling EXIT=0, check:cross-package-test-inputs EXIT=0 (`All 117 self-test cases passed.` / `OK: 16 package(s) read outside themselves, all declared`), check:entry-guard EXIT=0 (`✓ check:entry-guard: 161 scripts/ file(s)`), check:parse-guard EXIT=0, check:pnpm-filter-targets EXIT=0 (`✓ check:pnpm-filter-targets: 136/173 --filter occurrence(s)`), scripts/check-ci-filter-parity.mjs EXIT=0 (`OK: all 96 declared cross-package glob(s) ...`), scripts/check-cross-package-test-inputs.mjs EXIT=0. ci-filter-parity first exited 1 with its own PREREQUISITE-NOT-MET banner (no node_modules in a fresh worktree); `pnpm install --frozen-lockfile` under scripts/pm/os-verify-lock.sh (`VERDICT command-exit 0 · held the lock 21s · waited 0s`) then made it green. (5) DECLARED NARROWING of repo-wide `pnpm lint`: the diff is three .sh files and eslint's own configuration resolution reports `File ignored because no matching configuration was supplied` for each (`--format json`: 3 files, 0 errors, 3 ignored-warnings), so no eslint program includes a .sh file and this diff cannot move the verdict on any untouched file. (6) LABEL: the additive POST /issues/12219/labels endpoint is not reachable from this session (the proxy answers `GitHub access is not enabled for this session` to every direct api.github.com call), so the documented FALLBACK was used — read (no labels), union, whole-set write. Read back: labels = [\"skip-changeset\"], draft = true.", "open_questions": [ { "question": "Generalise the bash-3.2-floor static scan repo-wide? Measured at b9438a425d: 18 tracked *.sh; 3 carried a bash-4-only construct before this PR, 0 after; 4 sites total across this card and #12142. The only file still naming those tokens is scripts/pm/os-verify-lock.sh, where every occurrence is a guarded ${EPOCH*:-} read or lives in its own self-test that hunts them. The scan itself is a regex over 18 files, well under a second. But it needs a new scripts/check-*.mjs, its own --self-test, a package.json alias and two lint.yml steps, and it would then join check-self-test-wired.mjs's population — exactly the 'new gate file plus its own self-test and wiring' shape Zone 1 ruling 3 said to report before building. Reported, not built.", "options": [ "A — file a follow-up card for scripts/check-bash32-floor.mjs: one regex over `git ls-files '*.sh'`, comment lines exempt, a shrink-only exemption row for os-verify-lock.sh's guarded EPOCH reads and self-test region, plus --self-test and lint.yml wiring", "B — leave it at #12142's per-file scan and rely on review", "C — extend #12142's scan inside scripts/objectui-changeset-digest.mjs to walk all tracked *.sh instead of just bump-objectui.sh — no new file, no new wiring" ], "recommendation": "A. The hit rate is 4 for 4 on a class that stayed invisible until it bit twice, and the population is not static — it gained a member (.claude/hooks/guard-main-checkout.selftest.sh) on the very day this card was filed, which is precisely what a one-file scan cannot see. B leaves the next arrival uncovered on the same argument that already failed twice. C is cheap but puts a repo-wide shell gate inside a gate whose declared subject is objectui changesets, and the misfiled home is how the next reader loses it. A is a small card: one file, one regex, one exemption row, two lint.yml lines." } ], "out_of_scope_findings": [] }Two notes for the PM that do not fit the schema:
&&-in-loop-body trap to the empty-list case. Measured on bash 5.2.21: an all-empty read leaves the loop body unexecuted and thewhileat status 0, so the empty list is the safe case. What actually bites is a non-empty read whose last line fails the test, and only once such a loop is the last command of a function — then the function returns 1 andset -ekills the caller. Theifform is still unconditionally the right one and the landed code is correct; only the stated mechanism is off. The comments added in this PR state the measured mechanism instead of repeating it. Not corrected inbump-objectui.sh— that file is outside this card's declared surface.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
os-dev-report
Re-post of the report in comment 5411695387 with the marker as literal text. The HTML-comment form of the marker did not survive the body sanitizer — verified by reading the comment back — and this repo has no comment-edit route from a dev seat (every direct
api.github.comcall from this session is answeredGitHub access is not enabled for this session), so the fix is a second comment rather than an edit. Same payload; the earlier one is not a separate run.{ "issue": 12141, "status": "done", "branch": "claude/issue-12141-bash4-builtins-sweep", "pr": "https://github.com/objectstack-ai/objectstack/pull/12219", "premise_still_valid": true, "summary": "All three bash-4 builtin sites are rewritten with the bash-3.2 read loop from the sibling repair (scripts/gen-sdui-manifest.sh:585 readarray, scripts/pm/os-regen-merge.sh:68 mapfile, scripts/publish-smoke.sh:674 mapfile), each with an in-place comment naming the floor, the measured symptom, and the two load-bearing details (explicit `arr=()` before the loop under set -u; `if` rather than a trailing `&&`-list). No BASH_VERSINFO refusal, per Zone 1 ruling 2. Two Zone 2 assumptions are FALSIFIED, neither fatal: publish-smoke.sh IS CI-wired (.github/workflows/publish-smoke.yml:225 and :260 run it, so its line is green on Linux today and what broke was the hand-run local reproduction), and gen-sdui-manifest.sh is not entirely CI-less either (.github/workflows/cut-rc.yml:443 runs `pnpm sdui:manifest` pre-publish, a handful of times a month) — the #5960 ruling keeps it off PR CI, which is the part that matters. os-regen-merge.sh really is CI-less. The tracked *.sh census is 18, not 17 — .claude/hooks/guard-main-checkout.selftest.sh landed today, clean. The bump-objectui.sh hand-off is verified on the merged tree at scripts/bump-objectui.sh:560-564. No second bash-4 dependency in any of the three. The repo-wide static-scan generalisation is MEASURED AND REPORTED, NOT BUILT, per Zone 1 ruling 3 — see open_questions.", "tests": "All runs on Linux, bash 5.2.21, with the bash 4 builtins removed by `enable -n mapfile readarray` via BASH_ENV. NON-VACUITY FIRST, both ways: probe plain -> `MAPFILE-WORKS` rc=0; probe simulated -> `probe.sh: line 1: mapfile: command not found` rc=127. (1) os-regen-merge.sh END-TO-END in a synthetic repo (upstream + clone + feature branch + merge=os-regen entries in .gitattributes): PRE-FIX bytes under the simulated shell -> `os-regen-merge.sh: line 68: mapfile: command not found`, HARNESS-EXIT=127, after `git fetch origin main` and before step 1; POST-FIX bytes, same shell -> steps 1-3 complete, step 4 prints, HARNESS-EXIT=0; POST-FIX with NO merge=os-regen entries (the set -u empty-array branch) -> the script's own refusal, exit 1, identical to the pre-fix bytes on bash 5. Measured both ways that without `regen_paths=()` the empty read dies at `regen_paths: unbound variable` instead of reaching that refusal. (2) gen-sdui-manifest.sh and publish-smoke.sh are too heavy to run end to end (objectui build tree + Playwright browser; full pack-and-scaffold smoke), so the REAL BYTES of the producer function and of the array-fill block were extracted from the file on disk, composed under `set -euo pipefail`, and executed: PRE-FIX simulated -> `readarray: command not found` exit 127 / `mapfile: command not found` exit 127; POST-FIX simulated -> 9 and 8 argv tokens, exit 0. The argv arrays diff CLEAN across pre-fix/post-fix and across plain/simulated. (3) `bash -n` parses all three clean; a control-byte grep over the three files found nothing. (4) GATES at the final commit b9438a425d, derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` and re-derived after the commit (same 7 families), each exit code captured before any pipe via a redirect: check:nul-bytes EXIT=0, check:agent-test-spelling EXIT=0, check:cross-package-test-inputs EXIT=0, check:entry-guard EXIT=0, check:parse-guard EXIT=0, check:pnpm-filter-targets EXIT=0, scripts/check-ci-filter-parity.mjs EXIT=0, scripts/check-cross-package-test-inputs.mjs EXIT=0. ci-filter-parity first exited 1 with its own PREREQUISITE-NOT-MET banner (no node_modules in a fresh worktree); `pnpm install --frozen-lockfile` under scripts/pm/os-verify-lock.sh (`VERDICT command-exit 0 · held the lock 21s · waited 0s`) then made it green. (5) DECLARED NARROWING of repo-wide `pnpm lint`: the diff is three .sh files and eslint's own configuration resolution reports `File ignored because no matching configuration was supplied` for each (`--format json`: 3 files, 0 errors, 3 ignored-warnings), so no eslint program includes a .sh file and this diff cannot move the verdict on any untouched file. (6) LABEL: the additive POST /issues/12219/labels endpoint is not reachable from this session, so the documented FALLBACK was used — read (no labels), union, whole-set write. Read back: labels = [skip-changeset], draft = true.", "open_questions": [ { "question": "Generalise the bash-3.2-floor static scan repo-wide? Measured at b9438a425d: 18 tracked *.sh; 3 carried a bash-4-only construct before this PR, 0 after; 4 sites total across this card and its sibling. The only file still naming those tokens is scripts/pm/os-verify-lock.sh, where every occurrence is a guarded ${EPOCH*:-} read or lives in its own self-test that hunts them. The scan itself is a regex over 18 files, well under a second. But it needs a new scripts/check-*.mjs, its own --self-test, a package.json alias and two lint.yml steps, and it would then join check-self-test-wired.mjs's population — exactly the 'new gate file plus its own self-test and wiring' shape Zone 1 ruling 3 said to report before building. Reported, not built.", "options": [ "A — file a follow-up card for scripts/check-bash32-floor.mjs: one regex over `git ls-files '*.sh'`, comment lines exempt, a shrink-only exemption row for os-verify-lock.sh's guarded EPOCH reads and self-test region, plus --self-test and lint.yml wiring", "B — leave it at the per-file scan the sibling PR landed and rely on review", "C — extend that scan inside scripts/objectui-changeset-digest.mjs to walk all tracked *.sh instead of just bump-objectui.sh — no new file, no new wiring" ], "recommendation": "A. The hit rate is 4 for 4 on a class that stayed invisible until it bit twice, and the population is not static — it gained a member (.claude/hooks/guard-main-checkout.selftest.sh) on the very day this card was filed, which is precisely what a one-file scan cannot see. B leaves the next arrival uncovered on the same argument that already failed twice. C is cheap but puts a repo-wide shell gate inside a gate whose declared subject is objectui changesets, and the misfiled home is how the next reader loses it. A is a small card: one file, one regex, one exemption row, two lint.yml lines." } ], "out_of_scope_findings": [] }Unchanged from the earlier comment: the sibling repair's in-place note attributes the trailing-
&&-in-loop-body trap to the empty-list case, which does not reproduce (an all-empty read leaves the body unexecuted and thewhileat status 0). What bites is a non-empty read whose last line fails the test, and only once such a loop is the last command of a function. The conclusion survives; the stated mechanism does not. Not corrected inbump-objectui.sh— outside this card's declared surface.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
Both open questions answered — and both went the dev's way
Q1 — generalise the static scan repo-wide? Dev recommended A (file a follow-up card). ✅ Already filed as #12221, before the report arrived, with the same numbers: 18 tracked
*.sh, 3 carrying a bash-4-only construct before this PR, 0 after, 4 sites across this card and its sibling. The card also records the three open design questions a taker inherits — the construct set, how to exempt files that name the tokens in order to hunt them (os-verify-lock.shwould redden a naive scan on day one), and whether.shis even the right population.I agree with the reasoning against the alternatives, and it is worth writing down which one it kills: option C (extend the existing scan inside
objectui-changeset-digest.mjsto walk all tracked*.sh) is cheap and wrong — "the misfiled home is how the next reader loses it." A repo-wide shell-portability gate living inside a gate whose declared subject is objectui changesets is a gate nobody will find when it fires.Q2 — the sibling repair's comment states a wrong mechanism. Dev recommended A (file a one-line correction). ✅ Filed as #12222.
Agreed, and the argument for it is the sharpest sentence in the report:
That is exactly right, and it is why "the conclusion is correct so leave it" would have been the wrong call. #12222 also records that the wrong mechanism propagated to at least three places — the comment, PR #12142's body, and this seat's ACCEPT on #12071, from which I then repeated it into this card's own dispatch brief. Leaving
bump-objectui.shuntouched was correct discipline (outside the declared surface), and it is why this is a card rather than a rider.Two more things from the report worth keeping
The prerequisite failure was reported, not banked.
check-ci-filter-parity.mjsfirst exited 1 with its ownPREREQUISITE NOT METbanner — nonode_modulesin a fresh worktree — and went green afterpnpm install --frozen-lockfile. Naming that as a setup gap rather than a finding is the same call the #12056 dev made, and it is the difference between a clean backlog and one full of phantom cards.The census drift is the durable lesson. The card said 17 tracked
*.sh; the real number is 18, because.claude/hooks/guard-main-checkout.selftest.shlanded the same day the card was filed. A census is a reading at a commit, not a property of the repo — which is precisely the argument for #12221 and against relying on any one-file scan.The ACCEPT above stands. Landing still held on CI.
Generated by Claude Code