fix(portability): replace bash 4 builtins absent from macOS bash 3.2

macOS ships bash 3.2 as /bin/bash, which `#!/usr/bin/env bash` resolves to
when no newer bash is on PATH. Two builtins the repo relies on do not exist
there, and both failed SILENTLY:

- `mapfile` in the three surgical-commit helpers left every array empty, so
  the scope guards passed on nothing (fail-OPEN) and the commits degraded to
  "nothing pending — no-op" while reporting success. deploy-commit.test.sh
  went 4/16; memory and doc commits simply never happened.
- `declare -A` in the session-start hook errored on every session and left
  each plugin cost at 0, so the passive-budget warning could never fire.

`_read_lines_into` is the portable equivalent of `mapfile`, space-safe and
resetting its target first — expanding a never-assigned array trips `set -u`
on bash < 4.4, which is how the empty arrays surfaced as "unbound variable".
Plugin costs move to a `case`.

source-scope.sh's header prescribed `mapfile` to its callers; it now shows
the read loop, and its own test plus run-reconcile.sh stop using the builtin.

deploy-commit 16/16, source-scope 34/34, run-reconcile 25 GREEN / 0 RED,
session-start stderr empty.
This commit is contained in:
2026-09-13 17:20:57 -04:00
parent a53a5a26a8
commit f3919b6ace
7 changed files with 82 additions and 24 deletions
+15 -2
View File
@@ -25,6 +25,19 @@
set -uo pipefail
# bash 3.2 (macOS /bin/bash) predates the `mapfile` builtin; this is the portable
# equivalent. Reads stdin's lines into the array named by $1, space-safe
# (IFS= read -r). The array is reset first, so empty input yields an empty array
# rather than a stale or unset one — `set -u` on bash < 4.4 trips on expanding
# an array that was never assigned.
_read_lines_into() {
local _name="$1" _line
eval "$_name=()"
while IFS= read -r _line; do
eval "$_name+=(\"\$_line\")"
done
}
_in_git_repo() { git rev-parse --git-dir >/dev/null 2>&1; }
# True (0) when the repo is in a state where we must NOT auto-commit:
@@ -89,7 +102,7 @@ commit_docs() {
# (doc-syncer must never patch .claude/ or CLAUDE.md). Abort the WHOLE commit and
# name the offenders — never filter-and-commit-the-rest (that masks the bug).
local violations
mapfile -t violations < <(_scope_violations "$@")
_read_lines_into violations < <(_scope_violations "$@")
if [ "${#violations[@]}" -gt 0 ]; then
{
echo "doc-commit: REFUSED — out-of-scope path(s) in the doc list (upstream BDR-022 violation):"
@@ -100,7 +113,7 @@ commit_docs() {
return 4
fi
local changed
mapfile -t changed < <(_changed_paths "$@")
_read_lines_into changed < <(_changed_paths "$@")
if [ "${#changed[@]}" -eq 0 ]; then
echo "doc-commit: nothing pending — no-op" >&2
return 0