scripts/pm/os-verify-lock.sh — the single entry point for the shared heavy-verify lock —
uses bash-4-only builtins behind a #!/usr/bin/env bash shebang. On macOS, where
/usr/bin/env bash is bash 3.2.57, it never acquires the lock and never runs the command
it was given: it enters an unbounded retry loop, emitting
scripts/pm/os-verify-lock.sh: line 334: mapfile: command not found
scripts/pm/os-verify-lock.sh: line 177: /tmp/os-heavy-verify.lock.q/<ticket>: No such file or directory
scripts/pm/os-verify-lock.sh: line 122: EPOCHSECONDS: unbound variable
forever, at a few iterations per second, while re-minting a queue ticket each pass.
Why this matters more than a broken helper
The wrapper is the one mechanism that serializes heavy verification, and every dev agent is
instructed to route every build and test through it. When it degrades this way the failure
is doubly bad:
- It never times out. There is no acquisition budget being exceeded — the acquire path
itself is what is broken — so the caller sits in it until something external kills it.
Nothing is built, nothing is tested, and the wrapper prints no verdict line, so a caller
reading VERDICT finds none. - It burns the very resource it exists to protect. Several spinning waiters compete for
CPU with whatever real build is running.
Measured 2026-08-20 on the macOS host: all three dev sessions of one dispatch round were
stuck in it simultaneously (ps showed two live os-verify-lock.sh processes spinning after
the third was killed), none having started its build. It is not intermittent and not
load-dependent — the first mapfile call fails on every invocation.
Where
mapfile — scripts/pm/os-verify-lock.sh:334 (bash 4.0+)EPOCHSECONDS — scripts/pm/os-verify-lock.sh:122 (bash 5.0+), fatal under set -u
Fix shape (author's call)
Two honest options, and the second is the one worth arguing for:
- Replace the two builtins with portable equivalents (a
while read loop for mapfile;
date +%s for EPOCHSECONDS), keeping bash 3.2 support. - Keep using them and add a version guard at the top —
if ((BASH_VERSINFO[0] < 4)); then
print what is required, how to get it (brew install bash), and exit non-zero. A
dev agent is told to trust this entry point's verdict; refusing loudly is a verdict, and
an unbounded silent spin is the one outcome the wrapper must never produce. A guard also
protects whatever bash-4 idiom the next edit reaches for.
Either way the guard in (2) is the load-bearing half: a portability fix alone leaves the
next mapfile free to reintroduce this exact failure mode.
Found while working #10219; the verification for that card was run without the lock (declared
in its PR).
scripts/pm/os-verify-lock.sh— the single entry point for the shared heavy-verify lock —uses bash-4-only builtins behind a
#!/usr/bin/env bashshebang. On macOS, where/usr/bin/env bashis bash 3.2.57, it never acquires the lock and never runs the commandit was given: it enters an unbounded retry loop, emitting
forever, at a few iterations per second, while re-minting a queue ticket each pass.
Why this matters more than a broken helper
The wrapper is the one mechanism that serializes heavy verification, and every dev agent is
instructed to route every build and test through it. When it degrades this way the failure
is doubly bad:
itself is what is broken — so the caller sits in it until something external kills it.
Nothing is built, nothing is tested, and the wrapper prints no verdict line, so a caller
reading
VERDICTfinds none.CPU with whatever real build is running.
Measured 2026-08-20 on the macOS host: all three dev sessions of one dispatch round were
stuck in it simultaneously (
psshowed two liveos-verify-lock.shprocesses spinning afterthe third was killed), none having started its build. It is not intermittent and not
load-dependent — the first
mapfilecall fails on every invocation.Where
mapfile—scripts/pm/os-verify-lock.sh:334(bash 4.0+)EPOCHSECONDS—scripts/pm/os-verify-lock.sh:122(bash 5.0+), fatal underset -uFix shape (author's call)
Two honest options, and the second is the one worth arguing for:
while readloop formapfile;date +%sforEPOCHSECONDS), keeping bash 3.2 support.if ((BASH_VERSINFO[0] < 4)); thenprint what is required, how to get it (
brew install bash), and exit non-zero. Adev agent is told to trust this entry point's verdict; refusing loudly is a verdict, and
an unbounded silent spin is the one outcome the wrapper must never produce. A guard also
protects whatever bash-4 idiom the next edit reaches for.
Either way the guard in (2) is the load-bearing half: a portability fix alone leaves the
next
mapfilefree to reintroduce this exact failure mode.Found while working #10219; the verification for that card was run without the lock (declared
in its PR).