#!/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