Merge bugfix/macos-gnu-coreutils into develop

This commit is contained in:
2026-09-15 21:43:07 -04:00
4 changed files with 52 additions and 10 deletions
+33 -1
View File
@@ -30,6 +30,13 @@
set -uo pipefail
TIMEOUT="${GATES_TIMEOUT:-120}"
# GNU coreutils' `timeout` ships on Linux but NOT on a stock macOS (Homebrew
# installs it as both `timeout` and `gtimeout`). Resolve it once: without it
# every check exits 127 and reports NOT-MET whatever the check actually did.
# Overridable, and with `-` not `:-` so an explicitly EMPTY value forces the
# pure-bash path — that is how the fallback gets exercised on a machine that
# does have the binary.
GATES_TIMEOUT_BIN="${GATES_TIMEOUT_BIN-$(command -v timeout || command -v gtimeout || true)}"
EVIDENCE_CAP=140
# Module-level parse tables, index-aligned. Bash has no record type; threading
@@ -165,9 +172,34 @@ _decisive() { # _decisive <combined-output>
# Fail-closed: exit 0 AND the marker. A nonzero process never passes because
# its error text happens to contain the expected token.
# Same contract as `timeout`: run the command, return 124 if it outruns <secs>.
# Pure-bash stand-in for a platform shipping neither binary, so the deadline
# stays real instead of silently degrading into "every gate NOT-MET".
_gates_timeout() { # _gates_timeout <secs> <cmd>...
local secs="$1"; shift
[ -n "$GATES_TIMEOUT_BIN" ] && { "$GATES_TIMEOUT_BIN" "$secs" "$@"; return $?; }
local waited=0 pid
"$@" &
pid=$!
while kill -0 "$pid" 2>/dev/null; do
if [ "$waited" -ge "$secs" ]; then
# Park the shell's stderr: bash announces a signal-killed job on ITS
# stderr, which the caller captures with 2>&1 and would read as output.
exec 3>&2 2>/dev/null
kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
exec 2>&3 3>&-
return 124
fi
sleep 1
waited=$((waited + 1))
done
wait "$pid"
}
_run_one() { # _run_one <idx>
local i="$1" out rc
out="$(timeout "$TIMEOUT" bash -c "${_CHECK[i]}" 2>&1)"
out="$(_gates_timeout "$TIMEOUT" bash -c "${_CHECK[i]}" 2>&1)"
rc=$?
_STATUS[i]="NOT-MET"
if [ "$rc" -eq 124 ]; then
+7 -4
View File
@@ -9,6 +9,9 @@ no() { echo " FAIL $1 — $2"; FAIL=$((FAIL+1)); }
# assert stdout of a command contains / omits a fixed string
has() { if printf '%s' "$2" | grep -qF -- "$3"; then ok "$1"; else no "$1" "missing: $3"; fi; }
hasnt(){ if printf '%s' "$2" | grep -qF -- "$3"; then no "$1" "forbidden: $3"; else ok "$1"; fi; }
# Octal permission bits. GNU stat spells it -c %a, BSD stat (macOS) -f %OLp;
# neither accepts the other's flag, so try one then the other.
perm() { stat -c '%a' "$1" 2>/dev/null || stat -f '%OLp' "$1"; }
echo "── tokenstore ──"
TMP="$(mktemp -d)"; STORE="$TMP/tokens.json"
@@ -23,9 +26,9 @@ has "list shows client-a" "$LIST" '"client-a"'
has "list shows client-b" "$LIST" '"client-b"'
has "list shows a property" "$LIST" 'sc-domain:a.com'
hasnt "list redacts refresh tokens" "$LIST" 'RT_AAA'
PERM="$(stat -c '%a' "$STORE")"
PERM="$(perm "$STORE")"
[ "$PERM" = "600" ] && ok "store file is 0600" || no "store file 0600" "got $PERM"
DPERM="$(stat -c '%a' "$(dirname "$STORE")")"
DPERM="$(perm "$(dirname "$STORE")")"
[ "$DPERM" = "700" ] && ok "store dir is 0700" || no "store dir 0700" "got $DPERM"
rm -rf "$TMP"
@@ -136,7 +139,7 @@ import safe_fetch as sf
try: sf.safe_fetch("file:///etc/passwd"); print("OK")
except sf.UnsafeTarget: print("REFUSED")')"
has "non-http scheme refused" "$SCHEME" 'REFUSED'
IMP="$(/bin/grep -E "^(import|from) " "$SD/safe_fetch.py" | /bin/grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse")"
IMP="$(grep -E "^(import|from) " "$SD/safe_fetch.py" | grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse" | tr -d ' ')"
[ "$IMP" = "0" ] && ok "safe_fetch is stdlib-only" || no "safe_fetch is stdlib-only" "$IMP non-stdlib imports"
hasnt "no requests dependency" "$(cat "$SD/safe_fetch.py")" 'import requests'
@@ -477,7 +480,7 @@ has "clear reports ok" "$CL" '"status": "ok"'
has "clear reports count" "$CL" '"cleared": 1'
L7="$(python3 "$SD/tokenstore.py" list --file "$S6")"
has "clear empties store" "$L7" '"accounts": []'
PERM6="$(stat -c '%a' "$S6")"
PERM6="$(perm "$S6")"
[ "$PERM6" = "600" ] && ok "store stays 0600 after clear" || no "store 0600 after clear" "got $PERM6"
# via the real fetch.sh dispatch layer
python3 "$SD/tokenstore.py" set --file "$S6" --label back --refresh-token RT_BACK \
+7 -4
View File
@@ -36,17 +36,20 @@ check T4-none "$(bash "$L" detect "$tmp/cpp" >/dev/null 2>&1; echo $?)" 1
check T5-missing "$(bash "$L" cache-status "$tmp/js" || true)" missing
mkdir -p "$tmp/js/.ctx7-cache"; touch "$tmp/js/.ctx7-cache/react-core.md"
check T6-fresh "$(bash "$L" cache-status "$tmp/js")" fresh
touch -d '10 days ago' "$tmp/js/.ctx7-cache/react-core.md"
# `touch -d '10 days ago'` is GNU-only; BSD touch (macOS) wants -t with an
# absolute stamp. perl's utime is the one spelling both platforms ship.
perl -e 'my $t = time - 10*86400; utime $t, $t, $ARGV[0]' \
"$tmp/js/.ctx7-cache/react-core.md"
check T7-stale "$(bash "$L" cache-status "$tmp/js" || true)" stale
# --- hook: fires once per session, silent on stable projects ---
hook() { printf '{"prompt":"add a hook","session_id":"%s","cwd":"%s"}' \
"$1" "$2" | TMPDIR="$tmp" bash "$H"; }
check H1-fires "$(hook s1 "$tmp/js" | grep -c 'Fast-moving')" 1
check H2-once "$(hook s1 "$tmp/js" | wc -l)" 0
check H3-cpp-quiet "$(hook s2 "$tmp/cpp" | wc -l)" 0
check H2-once "$(hook s1 "$tmp/js" | wc -l | tr -d ' ')" 0
check H3-cpp-quiet "$(hook s2 "$tmp/cpp" | wc -l | tr -d ' ')" 0
check H4-notif-quiet \
"$(printf '{"prompt":"<task-notification>x","session_id":"s3","cwd":"%s"}' \
"$tmp/js" | TMPDIR="$tmp" bash "$H" | wc -l)" 0
"$tmp/js" | TMPDIR="$tmp" bash "$H" | wc -l | tr -d ' ')" 0
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+5 -1
View File
@@ -38,7 +38,11 @@ echo
( cd "$WORK" || exit 1
bash "$GITFLOW" start release 4.0.0 >/dev/null # base develop → release/4.0.0 (lib L49/L71)
printf '4.0.0\n' > version.txt # prep: version bump
sed -i 's/## \[Unreleased\]/## [Unreleased]\n\n## [4.0.0] — 2026-06-30/' CHANGELOG.md
# awk, not `sed -i`: BSD sed (macOS) needs a suffix argument after -i AND
# does not expand \n in the replacement — the edit silently did nothing
# there, so the CHANGELOG assertion below failed for the wrong reason.
awk '{ print; if ($0 == "## [Unreleased]") { print ""; print "## [4.0.0] — 2026-06-30" } }' \
CHANGELOG.md > CHANGELOG.tmp && cat CHANGELOG.tmp > CHANGELOG.md && rm -f CHANGELOG.tmp
git commit -qam "chore(release): 4.0.0 — version.txt + CHANGELOG"
bash "$GITFLOW" finish >/dev/null # fan-out main+develop+delete (lib L108-111)
# TAG = the gap. Lives in the SKILL (lib untouched). RED skips it, GREEN does it.