feat(check): every skill states when it stops and hands back
Implements #5's decision. The hard part was #37's, not #5's: which skills need an escalation rule? A hardcoded list rots, sniffing the prose for "does this write anything" is unfalsifiable, and a frontmatter flag is the option #5 rejected wearing a hat. So there is no acting set. Every skill answers the question, and a read-only one answers it in a line. That turns an absence into a claim someone can disagree with, which is the whole point — "this skill never hands back" is a design decision worth stating, and today it was indistinguishable from nobody having thought about it. Eleven written here, all advisory or read-only, where the honest answer is short and the value is in being specific rather than generic. `cognitive-load` stops when the metric disagrees with the people reading the code, because that is evidence about the metric. `test-design` stops when the tests are fine and the design under test is what makes them awkward. `code-review` stops when the diff is big enough that a review would be a skim, and says so rather than producing confident coverage of a fraction. The remaining thirty are the acting set — writes, dispatches, credentials, releases — and each needs a rule someone thought about rather than a rote paragraph, so they sit in bin/escalation-pending.txt. That list only shrinks, and it is self-cleaning: a listed skill that HAS a section fails the gate, so an exemption cannot outlive its reason. Mutation-verified both ways. Six test cases including the stale-entry case, because an exemption for a skill that no longer exists is how a gate quietly stops covering something. Refs #37, closes the mechanism half Bump-Type: minor
This commit is contained in:
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/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
|
||||
|
||||
exit $fail
|
||||
Reference in New Issue
Block a user