feat(kyberforge): enforce the ADR-0020 context contract for skills and agents
Skill name+description pairs are preloaded into every session, costing ~6,200 tokens across 39 skills before any skill is invoked. The authoring rules mandated that growth: skill-author:104 and description-quality.md:21 both required padding, while skill-author:102 (the deflating rule) had no FAIL condition behind it. Gates (blocking, no baseline file): - description 250 chars SUGGESTION / 400 FAIL, measured on the folded YAML value - body-only 600 words SUGGESTION / 900 FAIL, independent of the unchanged whole-file 2770-word / 500-line spec backstop - every boundary-clause routing target must resolve to a real skill or agent; catches skill-improve, neuledge-context and gitea-labels - agents take the description gates but deliberately no body gate; a test pins that absence Vale: DescriptionOpener widened to ^This\b, new CompositionNote rule banning architecture notes from descriptions. 10 hits, 0 false positives. Kyberforge's own four skills retrofitted: descriptions 3,364 -> 938 chars (-72%), bodies 8,306 -> 2,487 words (-70%), all via the apm-workflow dispatch pattern. Fixes the skill-improve dangling route and the agent-author misroute to manual review. Also fixes a pre-existing false positive where any line-initial 'read ' was flagged as interactive input, which had already caused two scripts to be rewritten around it. Refs: ADR-0020
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for scripts/skill-size-check.sh: enforces agentskills.io's
|
||||
# 500-line/5,000-token SKILL.md size ceiling. The token half is enforced via a
|
||||
# word-count proxy (MAX_WORDS, currently 2770) — 5,000 is the token ceiling,
|
||||
# 2,770 is the word budget the script derives from it at the corpus's densest
|
||||
# measured prose.
|
||||
# Regression test for scripts/skill-size-check.sh, which enforces two
|
||||
# independent gate families that must not be conflated:
|
||||
#
|
||||
# * agentskills.io spec conformance — 500 lines and a 5,000-token ceiling
|
||||
# enforced via a word-count proxy (MAX_WORDS, currently 2770) over the
|
||||
# WHOLE FILE, frontmatter included.
|
||||
# * ADR-0020 context budget — description 250 chars SUGGESTION / 400 FAIL,
|
||||
# body-ONLY 600 words SUGGESTION / 900 FAIL, and resolvable boundary-clause
|
||||
# routing targets.
|
||||
#
|
||||
# The constant-agreement block below is the load-bearing part: all three copies
|
||||
# (this hook, skill-audit's validate.sh, agent-audit's validate.sh) are
|
||||
# hand-duplicated because a cache-installed plugin cannot read outside its own
|
||||
# directory, and nothing but these assertions stops them drifting.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh"
|
||||
AGENT_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
@@ -92,6 +102,68 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 constants
|
||||
# ---------------------------------------------------------------------------
|
||||
# Three hand-maintained copies, for the same cache-isolation reason as
|
||||
# MAX_WORDS/MAX_LINES above. skill-audit carries all four; agent-audit carries
|
||||
# only the two description constants, because ADR-0020 deliberately gives
|
||||
# agents NO body word gate (a skill body competes with the caller's live
|
||||
# conversation; an agent body becomes the system prompt of a fresh context).
|
||||
# The absence of BODY_* in agent-audit is asserted below so a well-meaning
|
||||
# "consistency" edit that adds them fails here rather than contradicting the
|
||||
# ADR silently.
|
||||
DESC_SUGGEST_CHARS="$(grep -oE '^DESC_SUGGEST_CHARS=[0-9]+' "$SCRIPT" | cut -d= -f2)"
|
||||
DESC_MAX_CHARS="$(grep -oE '^DESC_MAX_CHARS=[0-9]+' "$SCRIPT" | cut -d= -f2)"
|
||||
BODY_SUGGEST_WORDS="$(grep -oE '^BODY_SUGGEST_WORDS=[0-9]+' "$SCRIPT" | cut -d= -f2)"
|
||||
BODY_MAX_WORDS="$(grep -oE '^BODY_MAX_WORDS=[0-9]+' "$SCRIPT" | cut -d= -f2)"
|
||||
|
||||
echo ""
|
||||
echo "--- the hook declares all four ADR-0020 constants ---"
|
||||
for pair in "DESC_SUGGEST_CHARS:$DESC_SUGGEST_CHARS" "DESC_MAX_CHARS:$DESC_MAX_CHARS" \
|
||||
"BODY_SUGGEST_WORDS:$BODY_SUGGEST_WORDS" "BODY_MAX_WORDS:$BODY_MAX_WORDS"; do
|
||||
if [[ -n "${pair#*:}" ]]; then
|
||||
pass "${pair%%:*}=${pair#*:}"
|
||||
else
|
||||
fail "${pair%%:*} is not declared in $SCRIPT"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- the hook and skill-audit's validate.sh agree on all four ADR-0020 constants ---"
|
||||
for const in DESC_SUGGEST_CHARS DESC_MAX_CHARS BODY_SUGGEST_WORDS BODY_MAX_WORDS; do
|
||||
hook_value="$(grep -oE "^${const}=[0-9]+" "$SCRIPT" | cut -d= -f2)"
|
||||
audit_value="$(grep -oE "^${const} = [0-9]+" "$VALIDATE" | grep -oE '[0-9]+' || true)"
|
||||
if [[ -n "$hook_value" && "$hook_value" == "$audit_value" ]]; then
|
||||
pass "both enforce $const=$hook_value"
|
||||
else
|
||||
fail "$const drift: hook says ${hook_value:-<unset>}, skill-audit validate.sh says ${audit_value:-<unset>}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- the hook and agent-audit's validate.sh agree on the description constants ---"
|
||||
if [[ ! -f "$AGENT_VALIDATE" ]]; then
|
||||
fail "agent-audit validate.sh not found at $AGENT_VALIDATE"
|
||||
else
|
||||
for const in DESC_SUGGEST_CHARS DESC_MAX_CHARS; do
|
||||
hook_value="$(grep -oE "^${const}=[0-9]+" "$SCRIPT" | cut -d= -f2)"
|
||||
agent_value="$(grep -oE "^${const} = [0-9]+" "$AGENT_VALIDATE" | grep -oE '[0-9]+' || true)"
|
||||
if [[ -n "$hook_value" && "$hook_value" == "$agent_value" ]]; then
|
||||
pass "both enforce $const=$hook_value"
|
||||
else
|
||||
fail "$const drift: hook says ${hook_value:-<unset>}, agent-audit validate.sh says ${agent_value:-<unset>}"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "--- agent-audit declares NO body word gate (ADR-0020 is explicit about this) ---"
|
||||
if grep -qE '^BODY_(SUGGEST|MAX)_WORDS = ' "$AGENT_VALIDATE"; then
|
||||
fail "agent-audit validate.sh declares a body word gate — ADR-0020 gives agents the description gates and NO body word gate"
|
||||
else
|
||||
pass "agent-audit validate.sh declares no BODY_*_WORDS constant"
|
||||
fi
|
||||
fi
|
||||
|
||||
# make_line_fixture builds a file with an exact total line count (frontmatter
|
||||
# included), independent of word count, for the line-boundary tests.
|
||||
make_line_fixture() {
|
||||
@@ -140,24 +212,40 @@ else
|
||||
fi
|
||||
|
||||
# make_word_fixture builds a file with an exact total word count (frontmatter
|
||||
# words included, since the script's `wc -w` counts the whole file) by padding
|
||||
# a body line with just enough "word" tokens to close the gap to the target.
|
||||
# words included, since the script's `wc -w` counts the whole file).
|
||||
#
|
||||
# The padding goes in a frontmatter `notes:` field, NOT in the body, and that
|
||||
# placement is the point: MAX_WORDS is a whole-file measurement while ADR-0020's
|
||||
# BODY_MAX_WORDS is a body-only one. Padding the body would make a 2,770-word
|
||||
# fixture trip the 900-word body ceiling too, and the MAX_WORDS boundary test
|
||||
# would stop isolating MAX_WORDS. `notes:` is an unused key — the description
|
||||
# stays short, so the description gate stays quiet as well.
|
||||
make_word_fixture() {
|
||||
local name="$1" target="$2" file cur remaining body
|
||||
local name="$1" target="$2" file frame_words padding
|
||||
file="$TMPDIR/$name.md"
|
||||
padding=""
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: Test fixture."
|
||||
echo "notes:$padding"
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Body."
|
||||
} > "$file"
|
||||
cur=$(wc -w < "$file")
|
||||
remaining=$((target - cur))
|
||||
body=""
|
||||
for ((i = 1; i <= remaining; i++)); do
|
||||
body="$body word"
|
||||
frame_words=$(wc -w < "$file")
|
||||
for ((i = 1; i <= target - frame_words; i++)); do
|
||||
padding="$padding word"
|
||||
done
|
||||
echo "$body" >> "$file"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: Test fixture."
|
||||
echo "notes:$padding"
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Body."
|
||||
} > "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
@@ -185,6 +273,189 @@ else
|
||||
pass "file at $((MAX_WORDS + 1)) words exits non-zero"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 behaviour
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# make_budget_fixture builds a SKILL.md with a verbatim description and an
|
||||
# exact BODY word count (frontmatter words excluded — the ADR-0020 body gate
|
||||
# counts the body only).
|
||||
make_budget_fixture() {
|
||||
local name="$1" desc="$2" body_words="$3" file
|
||||
file="$TMPDIR/$name.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: $desc"
|
||||
echo "---"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * $body_words))"
|
||||
} > "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
# expect_gate <label> <expected: pass|suggest|fail> <file> [needle]
|
||||
expect_gate() {
|
||||
local label="$1" expected="$2" file="$3" needle="${4:-}" out status
|
||||
set +e
|
||||
out="$("$SCRIPT" "$file" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
case "$expected" in
|
||||
pass)
|
||||
if [[ $status -eq 0 && -z "$out" ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
suggest)
|
||||
if [[ $status -eq 0 && "$out" == *"SUGGESTION"* && "$out" == *"$needle"* ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
fail)
|
||||
if [[ $status -ne 0 && "$out" == *"$needle"* ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "--- description budget: $DESC_SUGGEST_CHARS SUGGESTION / $DESC_MAX_CHARS FAIL, both inclusive ---"
|
||||
D_AT_SUGGEST="$(python3 -c "print('x' * $DESC_SUGGEST_CHARS)")"
|
||||
D_OVER_SUGGEST="$(python3 -c "print('x' * $((DESC_SUGGEST_CHARS + 1)))")"
|
||||
D_AT_MAX="$(python3 -c "print('x' * $DESC_MAX_CHARS)")"
|
||||
D_OVER_MAX="$(python3 -c "print('x' * $((DESC_MAX_CHARS + 1)))")"
|
||||
expect_gate "description at exactly $DESC_SUGGEST_CHARS chars is silent" \
|
||||
pass "$(make_budget_fixture desc-at-suggest "$D_AT_SUGGEST" 10)"
|
||||
expect_gate "description at $((DESC_SUGGEST_CHARS + 1)) chars suggests and exits 0" \
|
||||
suggest "$(make_budget_fixture desc-over-suggest "$D_OVER_SUGGEST" 10)" \
|
||||
"description is $((DESC_SUGGEST_CHARS + 1)) characters"
|
||||
expect_gate "description at exactly $DESC_MAX_CHARS chars suggests, does not fail" \
|
||||
suggest "$(make_budget_fixture desc-at-max "$D_AT_MAX" 10)" \
|
||||
"description is $DESC_MAX_CHARS characters"
|
||||
expect_gate "description at $((DESC_MAX_CHARS + 1)) chars fails" \
|
||||
fail "$(make_budget_fixture desc-over-max "$D_OVER_MAX" 10)" \
|
||||
"$DESC_MAX_CHARS-character ceiling"
|
||||
|
||||
echo ""
|
||||
echo "--- description length is measured after YAML folding is resolved ---"
|
||||
FOLDED="$TMPDIR/folded.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: folded"
|
||||
echo "description: >"
|
||||
python3 -c "print('\n'.join([' ' + 'x' * 40] * 11))"
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Do the thing."
|
||||
} > "$FOLDED"
|
||||
expect_gate "a >-folded 450-char description fails (raw first line would read as 1 char)" \
|
||||
fail "$FOLDED" "description is 450 characters"
|
||||
|
||||
echo ""
|
||||
echo "--- body budget: $BODY_SUGGEST_WORDS SUGGESTION / $BODY_MAX_WORDS FAIL, body only, both inclusive ---"
|
||||
expect_gate "body at exactly $BODY_SUGGEST_WORDS words is silent" \
|
||||
pass "$(make_budget_fixture body-at-suggest "Short valid description." "$BODY_SUGGEST_WORDS")"
|
||||
expect_gate "body at $((BODY_SUGGEST_WORDS + 1)) words suggests and exits 0" \
|
||||
suggest "$(make_budget_fixture body-over-suggest "Short valid description." "$((BODY_SUGGEST_WORDS + 1))")" \
|
||||
"body is $((BODY_SUGGEST_WORDS + 1)) words"
|
||||
expect_gate "body at exactly $BODY_MAX_WORDS words suggests, does not fail" \
|
||||
suggest "$(make_budget_fixture body-at-max "Short valid description." "$BODY_MAX_WORDS")" \
|
||||
"body is $BODY_MAX_WORDS words"
|
||||
expect_gate "body at $((BODY_MAX_WORDS + 1)) words fails" \
|
||||
fail "$(make_budget_fixture body-over-max "Short valid description." "$((BODY_MAX_WORDS + 1))")" \
|
||||
"$BODY_MAX_WORDS-word ceiling"
|
||||
|
||||
# The two word gates measure different things and must stay separable: a file
|
||||
# whose FRONTMATTER pushes the whole-file count past the body ceiling must not
|
||||
# trip the body gate, and a file under MAX_WORDS can still fail the body gate.
|
||||
echo ""
|
||||
echo "--- the body gate and the whole-file gate are independent measurements ---"
|
||||
BODY_ONLY_DESC="$(python3 -c "print(' '.join(['w'] * 100))")"
|
||||
expect_gate "frontmatter words do not count toward the $BODY_MAX_WORDS-word body ceiling" \
|
||||
suggest "$(make_budget_fixture body-independent "$BODY_ONLY_DESC" "$((BODY_MAX_WORDS - 5))")" \
|
||||
"words"
|
||||
BIG_BODY="$(make_budget_fixture body-over-not-whole-file "Short valid description." "$((BODY_MAX_WORDS + 1))")"
|
||||
BIG_BODY_WORDS="$(wc -w < "$BIG_BODY")"
|
||||
if [[ "$BIG_BODY_WORDS" -le "$MAX_WORDS" ]]; then
|
||||
pass "the body-gate fixture is $BIG_BODY_WORDS whole-file words, well under MAX_WORDS=$MAX_WORDS — it fails on the body gate alone"
|
||||
else
|
||||
fail "the body-gate fixture is $BIG_BODY_WORDS whole-file words, which also trips MAX_WORDS=$MAX_WORDS — the test no longer isolates the body gate"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- resolvable boundary targets ---"
|
||||
# Resolution is against the AUTHORING SOURCE (plugins/*/.apm/skills/ and
|
||||
# plugins/*/.apm/agents/), found here via the script's own repo root — these
|
||||
# fixtures live in a temp dir with no plugin tree of their own, so a resolving
|
||||
# target proves the repo-root path works.
|
||||
expect_gate "a boundary target naming a real skill resolves" \
|
||||
pass "$(make_budget_fixture target-ok \
|
||||
"Use when doing the thing. Do not use for commits — use git-commits instead." 10)"
|
||||
expect_gate "a boundary target naming a real AGENT resolves (agents are valid targets)" \
|
||||
pass "$(make_budget_fixture target-agent-ok \
|
||||
"Use when doing the thing. Do not use when the caller is an agent — invoke git-orchestrate instead." 10)"
|
||||
expect_gate "a boundary target that resolves to nothing fails" \
|
||||
fail "$(make_budget_fixture target-missing \
|
||||
"Use when doing the thing. Do not use for improvements — use no-such-skill-anywhere instead." 10)" \
|
||||
"routes to 'no-such-skill-anywhere'"
|
||||
expect_gate "a /slash-command boundary target that resolves to nothing fails" \
|
||||
fail "$(make_budget_fixture target-missing-slash \
|
||||
"Use when doing the thing. Do not use for improvements — use /no-such-slash-skill instead." 10)" \
|
||||
"routes to 'no-such-slash-skill'"
|
||||
# False-positive guards. These phrasings are lifted from real descriptions:
|
||||
# pc-run says "run pre-commit hooks", diagnose chains "fix -> regression-test",
|
||||
# gitea-files says "(use Read/Write/Edit)", gitea-labels-milestones says
|
||||
# "through `issue_write`/`pull_request_write`". None of them is a routing
|
||||
# target, and reading any of them as one makes the gate untrustworthy.
|
||||
expect_gate "'run pre-commit hooks' outside a boundary sentence is not a routing target" \
|
||||
pass "$(make_budget_fixture fp-precommit \
|
||||
"Use when the user wants to run pre-commit hooks or install git hooks." 10)"
|
||||
expect_gate "an arrow chain outside a boundary clause is not a routing target" \
|
||||
pass "$(make_budget_fixture fp-arrow \
|
||||
"Reproduce → minimise → instrument → fix → regression-test. Use when a bug is reported." 10)"
|
||||
expect_gate "tool names and MCP tool names are not routing targets" \
|
||||
pass "$(make_budget_fixture fp-tools \
|
||||
"Use when writing issues. Do not use for local files (use Read/Write/Edit) — that write goes through \`issue_write\`/\`pull_request_write\` instead." 10)"
|
||||
|
||||
echo ""
|
||||
echo "--- the three live dangling routing targets are caught (issue #100) ---"
|
||||
# ADR-0020 records four broken routing targets and splits fixing them into its
|
||||
# own issue. Three are detectable from the description text alone; this asserts
|
||||
# the gate actually sees them rather than the check being vacuous in the corpus
|
||||
# it was written against.
|
||||
for probe in \
|
||||
"plugins/bin/.apm/skills/research/SKILL.md:neuledge-context" \
|
||||
"plugins/kyberforge/.apm/skills/skill-audit/SKILL.md:skill-improve" \
|
||||
"plugins/gitea/.apm/skills/gitea-issues/SKILL.md:gitea-labels"; do
|
||||
probe_file="$REPO_ROOT/${probe%%:*}"
|
||||
probe_name="${probe##*:}"
|
||||
if [[ ! -f "$probe_file" ]]; then
|
||||
pass "SKIP: ${probe%%:*} no longer exists (retrofitted)"
|
||||
continue
|
||||
fi
|
||||
# Captured, not piped: the script exits non-zero on these files and
|
||||
# `set -o pipefail` would make the whole pipeline non-zero regardless of what
|
||||
# grep found.
|
||||
set +e
|
||||
probe_out="$("$SCRIPT" "$probe_file" 2>&1)"
|
||||
set -e
|
||||
if [[ "$probe_out" == *"routes to '$probe_name'"* ]]; then
|
||||
pass "detects the dangling '$probe_name' target in ${probe%%:*}"
|
||||
elif ! grep -q "$probe_name" "$probe_file"; then
|
||||
pass "SKIP: '$probe_name' no longer appears in ${probe%%:*} (fixed by issue #100)"
|
||||
else
|
||||
fail "did not detect the dangling '$probe_name' target in ${probe%%:*}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
|
||||
@@ -145,6 +145,81 @@ kyberforge-vale-audit-skill|skills/demo/SKILL.md|helps with
|
||||
kyberforge-vale-audit-agent|agents/demo.md|utilize
|
||||
EOF
|
||||
|
||||
# --- 1b. Every rule in the shipped style is asserted to FIRE, not merely to
|
||||
# exist. A Vale rule can be well-formed, load without a diagnostic, and match
|
||||
# nothing at all: `extends: existence` CONCATENATES multiple `raw:` entries
|
||||
# rather than alternating them, so a rule written as a list of alternatives
|
||||
# silently becomes one impossible expression, lints every file clean and exits
|
||||
# 0 — indistinguishable from a corpus with no violations. `Kyberforge.CompositionNote`
|
||||
# was written that way first and passed all 43 skill and agent files before the
|
||||
# defect was found by hand. Each rule gets its own fixture pass, with the token
|
||||
# it must quote attributed to the file that raised it, so one rule's alert can
|
||||
# never stand in for another's.
|
||||
echo ""
|
||||
echo "--- each Kyberforge description rule fires through both shipped hooks ---"
|
||||
write_desc_fixtures() {
|
||||
local skill_desc="$1" agent_desc="$2"
|
||||
cat > "$CONSUMER/skills/demo/SKILL.md" <<EOF
|
||||
---
|
||||
name: demo
|
||||
description: >
|
||||
$skill_desc across two
|
||||
physical lines of one folded block scalar.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
cat > "$CONSUMER/agents/demo.md" <<EOF
|
||||
---
|
||||
name: demo
|
||||
description: >
|
||||
$agent_desc across two
|
||||
physical lines of one folded block scalar.
|
||||
---
|
||||
|
||||
Body.
|
||||
EOF
|
||||
git -C "$CONSUMER" add -A
|
||||
}
|
||||
|
||||
run_rule_case() {
|
||||
local label="$1" hook_id="$2" fixture="$3" token="$4"
|
||||
local log="$WORK/rule-$label.log"
|
||||
set +e
|
||||
(cd "$CONSUMER" && pre-commit run "$hook_id" --all-files > "$log" 2>&1)
|
||||
local rc=$?
|
||||
set -e
|
||||
if grep -q "Skipped" "$log"; then
|
||||
fail "$hook_id matched no files for $label, so it proved nothing"
|
||||
sed 's/^/ /' "$log"
|
||||
elif [[ $rc -eq 0 ]]; then
|
||||
fail "$hook_id passed $fixture despite its flagged '$token' — $label matches nothing"
|
||||
sed 's/^/ /' "$log"
|
||||
elif alerts_for "$fixture" < "$log" | grep -qF "'$token'"; then
|
||||
pass "$label fires through $hook_id and quotes '$token' under $fixture"
|
||||
else
|
||||
fail "$hook_id failed, but no $label alert quoting '$token' was filed under $fixture"
|
||||
sed 's/^/ /' "$log"
|
||||
fi
|
||||
}
|
||||
|
||||
# CompositionNote: a distinct banned token per file shape.
|
||||
write_desc_fixtures \
|
||||
"Use when the caller wants a demo skill that composes other skills" \
|
||||
"Use when the caller wants a cross-cutting demo agent"
|
||||
run_rule_case "Kyberforge.CompositionNote" kyberforge-vale-audit-skill skills/demo/SKILL.md "composes"
|
||||
run_rule_case "Kyberforge.CompositionNote" kyberforge-vale-audit-agent agents/demo.md "cross-cutting"
|
||||
|
||||
# DescriptionOpener: the widened pattern catches every non-imperative "This..."
|
||||
# opener, not only the literal "This skill"/"This agent" pair it was anchored to
|
||||
# before. Both fixtures open with "This is", the form two shipped descriptions
|
||||
# used mid-sentence and which the old pattern could not express.
|
||||
write_desc_fixtures \
|
||||
"This is a demo skill for callers who want one" \
|
||||
"This is a demo agent for callers who want one"
|
||||
run_rule_case "Kyberforge.DescriptionOpener" kyberforge-vale-audit-skill skills/demo/SKILL.md "This"
|
||||
run_rule_case "Kyberforge.DescriptionOpener" kyberforge-vale-audit-agent agents/demo.md "This"
|
||||
|
||||
# --- 2. Clean files pass — the hooks gate, they don't just always fail ---
|
||||
echo ""
|
||||
echo "--- all three hooks pass clean files in an external consumer repo ---"
|
||||
|
||||
Reference in New Issue
Block a user