Files
boshu2__agentops/scripts/check-shell-exec-bits.sh
Bo e556834d3d Legible membrane, Train 1: Codex projection, executable entry points, green run-all, honest runtime docs (#1097)
## Legible membrane, Train 1: fix what ships

Intent source: `docs/plans/2026-09-02-legible-membrane-plan.md` (added
in this PR). Provenance: the 2026-09-02 field audit of this repo against
mattpocock/skills, compound-engineering, and the jsm corpus.

Three defects a stranger hits today, all verified on `main` before the
change:

1. **Codex projection truncated 51 of 56 skill descriptions mid-clause**
(`scripts/codex-sync.sh` capped prose at 44 chars, so the Codex router
read "Freshly judge whether a finished change is Triggers: …"). Now:
first sentence of the source prose plus the full `Triggers:` clause,
abbreviation- and quote-aware; the per-catalog bound is computed live as
"Codex prose average may not exceed Claude's" (cross-multiplied,
floor-free) with a 180-char hard ceiling. `using-flywheel` is exempted
from runtime-phrase rewriting so its cross-runtime text projects
verbatim. The dormant `ao codex ensure-start` emitter and the `# /x`→`#
$x` title rewrite are removed. A literal oracle pins five twins and a
rule oracle covers all 56.
2. **23 shebang-bearing shell entry points were committed
non-executable**, including `scripts/regen-all.sh` and three gate
scripts. Now `100755`, with an advisory gate `shell.exec-bits`
(fast+full, fail-closed on enumeration error, reads the index blob,
skips symlinks by stated policy).
3. **`tests/run-all.sh` was red on `main`**: the GOALS validator
asserted a pre-08-25 file shape, and validate's description exceeded the
180-char budget. The validator now parses only the `## Gates` block
(stops at any heading, like production), requires cited script paths to
exist, takes its path via argv, and fails closed on tokenization; four
negative fixtures. validate's description is 177 chars and gains the
reality-check negative. `AGENTS.md` quotes CI's authoritative bats and
Go commands.

README and `docs/install-day2-ops.md` replace "No other runtime is
required" with a runtime table derived by reading each skill's procedure
(HARD / OPTIONAL / conditional), covered by a bats test that grounds
every HARD row in an invocation line.

Evidence on the tip: full gate 71/71 (HEAD binary), CI's bats command
green across `tests/scripts/*.bats`, `tests/run-all.sh` green, Go
build/vet/test green, golangci-lint clean, security gate quick PASS,
`scripts/regen-all.sh --check` clean. Each lane was validated by a fresh
context; the integrated train had two cross-family (Codex) review rounds
and every finding is closed on this tip.

Out of scope (successor intent): promoted set / `skills-internal/`, the
process-artifact sweep, "It's working if" blocks, routing clusters,
doctrine diet. One pre-existing drift noted for a follow-up:
`packs/agentops-executor/agents/validator/skills/validate/SKILL.md`
carries a stale third description that no gate binds.
2026-09-02 23:29:33 +00:00

173 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# check-shell-exec-bits.sh — ADVISORY guard: the shell entry points this
# repository documents must actually be executable as documented.
#
# WHY: AGENTS.md tells a contributor to run `scripts/regen-all.sh --check` and
# `tests/run-all.sh`. On 2026-09-02 twenty-three tracked shebang-bearing shell
# files under scripts/ and tests/ carried index mode 100644, so
# `./scripts/regen-all.sh --check` answered "permission denied" — the
# documented command did not run at all. A missing exec bit is invisible to
# every other gate (shellcheck, the bats suites, and CI all invoke scripts
# through an explicit `bash`), so nothing caught it.
#
# WHAT (fail = exit 1, offenders listed with the repair command):
# 1. every tracked *.sh under scripts/ or tests/ whose FIRST LINE is a
# shebang must have tracked mode 100755;
# 2. every tracked *.sh under scripts/ or tests/ WITHOUT a shebang must live
# under a `lib/` directory — those are sourced libraries, correctly 644,
# and `lib/` is the convention that says so.
#
# The tracked (index) mode is what a fresh clone gets, so that is what is
# checked; a working-tree-only `chmod` that was never staged is still broken
# for everyone else.
#
# Usage:
# bash scripts/check-shell-exec-bits.sh # check this repo
# bash scripts/check-shell-exec-bits.sh <repo-dir> # check another checkout
#
# Both the mode and the first line are read from the INDEX, never from the
# working tree: an unstaged edit must not be able to hide a broken indexed
# entry (and the index is what a fresh clone gets). Index entries with mode
# 120000 are symlinks; a symlinked *.sh is not an entry point, so they are
# skipped with a printed note rather than judged.
#
# Exit codes: 0 clean; 1 violations found; 2 enumeration failed (not a git
# repository, git error, or nothing to check); 127 missing git.
# shellcheck disable=SC1007
# shellcheck source=scripts/lib/preamble.sh
. "$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/preamble.sh"
require_cmd git
PROG="check-shell-exec-bits"
note() { printf '[%s] %s\n' "$PROG" "$*"; }
# in_lib_dir PATH → true when any directory component of PATH is exactly "lib".
in_lib_dir() {
case "/$1" in
*/lib/*) return 0 ;;
*) return 1 ;;
esac
}
# check_repo REPO → 0 when every tracked shell file under scripts/ and tests/
# obeys both rules, 1 when a rule is broken, 2 when enumeration itself failed.
check_repo() {
local repo="$1" rc=0 mode path first blob scanned=0
local -a missing_exec=() stray_sourced=() symlinked=()
local work index_z err_log
# Enumeration runs through a CHECKED path. It used to sit behind a process
# substitution, which discards git's exit status entirely: pointing the gate
# at a non-repository printed git's fatal error, enumerated nothing, and
# still reported OK with exit 0 — a gate that fails open is worse than no
# gate. NUL-delimited so a path containing whitespace or a newline cannot
# split a record.
with_tmpdir work shell-exec-bits
index_z="$work/index.z"
err_log="$work/git.err"
if ! git -C "$repo" ls-files -s -z > "$index_z" 2> "$err_log"; then
note "FAIL: could not enumerate tracked files in $repo (git ls-files failed)"
sed 's/^/ /' "$err_log" >&2 || true
return 2
fi
while IFS= read -r -d '' line; do
[ -n "$line" ] || continue
# `git ls-files -s -z` → "<mode> <sha> <stage>\t<path>\0"
mode="${line%% *}"
path="${line#*$'\t'}"
case "$path" in
scripts/* | tests/*) ;;
*) continue ;;
esac
case "$path" in
*.sh) ;;
*) continue ;;
esac
scanned=$((scanned + 1))
# 120000 is a symlink entry. A symlinked *.sh is not an entry point of
# this repository; judging its "shebang" would read the link target text.
if [ "$mode" = "120000" ]; then
symlinked+=("$path")
continue
fi
# First line from the INDEX blob, not the working tree. No pipe into
# `head`: under `set -o pipefail` the early reader would SIGPIPE git and
# turn a readable blob into a spurious failure.
if ! blob="$(git -C "$repo" cat-file -p ":$path" 2>/dev/null)"; then
note "FAIL: could not read the indexed blob for $path"
rc=1
continue
fi
first="${blob%%$'\n'*}"
case "$first" in
'#!'*)
if [ "$mode" != "100755" ]; then
missing_exec+=("$mode $path")
fi
;;
*)
if ! in_lib_dir "$path"; then
stray_sourced+=("$mode $path")
fi
;;
esac
done < "$index_z"
# Fail closed on an empty population: "checked nothing" must never read as
# "checked everything and it was fine".
if [ "$scanned" -eq 0 ]; then
note "FAIL: nothing enumerated — no tracked *.sh under scripts/ or tests/ in $repo"
return 2
fi
if [ "${#symlinked[@]}" -gt 0 ]; then
note "note: ${#symlinked[@]} symlinked *.sh entr(y|ies) skipped (a symlink is not an entry point):"
printf ' %s\n' "${symlinked[@]}"
fi
if [ "${#missing_exec[@]}" -gt 0 ]; then
note "FAIL: ${#missing_exec[@]} shebang-bearing shell file(s) are not tracked executable:"
printf ' %s\n' "${missing_exec[@]}"
note " repair: git update-index --chmod=+x <path> (and chmod +x <path> in the working tree)"
rc=1
fi
if [ "${#stray_sourced[@]}" -gt 0 ]; then
note "FAIL: ${#stray_sourced[@]} shell file(s) without a shebang live outside a lib/ directory:"
printf ' %s\n' "${stray_sourced[@]}"
note " repair: add a '#!/usr/bin/env bash' shebang and the exec bit, or move the sourced library under a lib/ directory"
rc=1
fi
if [ "$rc" -eq 0 ]; then
note "OK: $scanned tracked *.sh under scripts/ and tests/ are executable as documented ($repo)"
fi
return "$rc"
}
usage() {
sed -n '2,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}
case "${1:-}" in
-h | --help)
usage
exit 0
;;
*)
target="${1:-$REPO_ROOT}"
# The exit status is passed through verbatim: 1 is "a rule was broken",
# 2 is "the check could not run" — collapsing them would hide the second.
check_repo "$target" || exit "$?"
exit 0
;;
esac