"That list only shrinks" was asserted in the commit message, the script header, the pending file's own header, the test header, and the issue comment. It was enforced nowhere. A traceability probe falsified it in one run: extract the commit to a temp tree, add a skill with no `## Escalation` section, append its name to bin/escalation-pending.txt — gate returns 0. The list could grow silently and forever. "Self-cleaning" was true; "only shrinks" was a social convention wearing a mechanism's clothes, and this repo has spent a day on what those cost. The gate now diffs the pending list against the version committed at HEAD and fails on any added entry, naming it. Where the comparison is impossible — not a git checkout — it says so rather than passing silently, because a ratchet that cannot be verified must not look like one that was. Four new cases: unchanged list, shrinking list, growing list, and the unverifiable case. Refs #37 Bump-Type: patch
89 lines
3.7 KiB
Bash
Executable File
89 lines
3.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# check-escalation — every skill states when it stops and hands back.
|
|
#
|
|
# #5 decided this belongs in the body, not the frontmatter: no harness reads
|
|
# frontmatter beyond name/description, so a `failure_criteria:` key would be a
|
|
# guarantee nothing honours. The model reads the body.
|
|
#
|
|
# #37 then had to define which skills need one, and every obvious answer was
|
|
# bad: a hardcoded list rots, sniffing the text for "does it write anything" is
|
|
# unfalsifiable, and a frontmatter flag is the rejected option wearing a hat.
|
|
#
|
|
# So there is no acting set. EVERY skill answers the question. A read-only skill
|
|
# answers it in one line — "read-only; findings are the output, nothing is
|
|
# handed back" — which turns an absence into a claim someone can disagree with.
|
|
#
|
|
# bin/escalation-pending.txt is a ratchet for skills whose real rule has not
|
|
# been written yet (a stop condition nobody thought about is not worth
|
|
# enforcing). It is self-cleaning: a listed skill that HAS a section fails, so
|
|
# the list cannot outlive its reason.
|
|
#
|
|
# Usage: check-escalation [checkout-root]
|
|
|
|
set -u
|
|
|
|
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
|
|
PENDING="$ROOT/bin/escalation-pending.txt"
|
|
fail=0
|
|
|
|
listed() {
|
|
[ -f "$PENDING" ] || return 1
|
|
sed 's/#.*//' "$PENDING" | grep -qx "[[:space:]]*$1[[:space:]]*"
|
|
}
|
|
|
|
for dir in "$ROOT"/skills/*/; do
|
|
[ -f "$dir/SKILL.md" ] || continue
|
|
name=$(basename "$dir")
|
|
|
|
if grep -q '^## Escalation' "$dir/SKILL.md"; then
|
|
if listed "$name"; then
|
|
echo "ERROR: skills/$name has an Escalation section but is still in bin/escalation-pending.txt — remove it from the list" >&2
|
|
fail=1
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
listed "$name" && continue
|
|
echo "ERROR: skills/$name has no '## Escalation' section — say when it stops and hands back, or say it never does" >&2
|
|
fail=1
|
|
done
|
|
|
|
# A pending entry for a skill that no longer exists is a stale exemption, and a
|
|
# stale exemption is how a gate quietly stops covering something. Looped without
|
|
# a pipe on purpose: a `while read` on the right of a pipe runs in a subshell,
|
|
# so `fail=1` would be set in a process that exits before it is read.
|
|
if [ -f "$PENDING" ]; then
|
|
entries=$(sed 's/#.*//' "$PENDING" | tr -d '[:blank:]' | grep -v '^$' || true)
|
|
for entry in $entries; do
|
|
[ -f "$ROOT/skills/$entry/SKILL.md" ] && continue
|
|
echo "ERROR: bin/escalation-pending.txt lists '$entry', which has no skills/$entry/SKILL.md" >&2
|
|
fail=1
|
|
done
|
|
fi
|
|
|
|
# --- the ratchet must actually ratchet ---------------------------------------
|
|
# "This list only shrinks" was asserted in five places and enforced in none. A
|
|
# traceability probe falsified it in one run: add an unsectioned skill, append it
|
|
# to the list, gate returns 0. So compare against the list as committed at HEAD —
|
|
# a convention nothing checks is a convention, and this repo has spent a day on
|
|
# what those cost.
|
|
if [ -f "$PENDING" ]; then
|
|
if ! git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
|
echo "WARN: $ROOT is not a git checkout — cannot verify the pending list only shrank" >&2
|
|
elif ! git -C "$ROOT" cat-file -e HEAD:bin/escalation-pending.txt 2>/dev/null; then
|
|
: # first commit introducing the file — nothing to compare against yet
|
|
else
|
|
before=$(git -C "$ROOT" show HEAD:bin/escalation-pending.txt 2>/dev/null \
|
|
| sed 's/#.*//' | tr -d '[:blank:]' | grep -v '^$' | sort -u || true)
|
|
now=$(sed 's/#.*//' "$PENDING" | tr -d '[:blank:]' | grep -v '^$' | sort -u || true)
|
|
added=$(printf '%s\n' "$now" | grep -vxF "$before" 2>/dev/null || true)
|
|
for entry in $added; do
|
|
[ -n "$entry" ] || continue
|
|
echo "ERROR: '$entry' was added to bin/escalation-pending.txt — the list only shrinks. Write its '## Escalation' section instead of exempting it." >&2
|
|
fail=1
|
|
done
|
|
fi
|
|
fi
|
|
|
|
exit $fail
|