refactor!: carry out the simplification audit across gates, tests, plugins and docs #135

Merged
Defame1297 merged 85 commits from docs/simplification-audit into main 2026-09-20 19:14:03 +00:00
9 changed files with 693 additions and 49 deletions
Showing only changes of commit ea119d83b0 - Show all commits

View File

@@ -215,7 +215,7 @@ repos:
- id: skill-size-check - id: skill-size-check
stages: ['pre-commit'] stages: ['pre-commit']
name: SKILL.md size and context-budget ceilings name: SKILL.md size and context-budget ceilings
description: Enforce agentskills.io's 500-line/2,770-whole-file-word spec ceilings AND ADR-0020's context budget -- description 250 chars SUGGESTION / 400 FAIL, body-only 600 words SUGGESTION / 900 FAIL, and every boundary-clause routing target resolving to a real skill or agent under plugins/*/.apm/ description: Enforce agentskills.io's 500-line/2,770-whole-file-word spec ceilings AND ADR-0020's context budget -- description 250 chars SUGGESTION / 400 FAIL, body-only 600 words SUGGESTION / 900 FAIL, and every boundary-clause routing target resolving to a real skill or agent under plugins/*/.apm/ -- plus the required frontmatter fields folded in from the former skill-frontmatter hook, namely name, a non-empty description, and a metadata.version matching three-part semver (1.0.0)
entry: scripts/skill-size-check.sh entry: scripts/skill-size-check.sh
language: script language: script
files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$'

View File

@@ -56,12 +56,21 @@ emit() {
# is churn unrelated to the branch and should be discarded. The branch name only # is churn unrelated to the branch and should be discarded. The branch name only
# selects between fixed strings and is never interpolated. Outside a git checkout, # selects between fixed strings and is never interpolated. Outside a git checkout,
# or on a detached HEAD, the neutral advice stands. # or on a detached HEAD, the neutral advice stands.
#
# So does an UNSET origin/HEAD, which is the common state: git only writes it on
# clone, and `git remote add` never does. The fallback here used to be `main`,
# which is a guess, and it is wrong in exactly the repos that would notice — a
# checkout whose default branch is `master` was told "this is a feature branch,
# so discard it" while standing on its default branch, i.e. told to throw away a
# real lock update. There is no cheap way to learn the remote's default without
# the network, so nothing is asserted: the advice stays neutral and the reader
# decides.
lock_advice="commit it or discard it deliberately." lock_advice="commit it or discard it deliberately."
current_branch="$(git symbolic-ref --short -q HEAD 2> /dev/null || true)" current_branch="$(git symbolic-ref --short -q HEAD 2> /dev/null || true)"
if [[ -n "$current_branch" ]]; then default_branch="$(git symbolic-ref --short -q refs/remotes/origin/HEAD 2> /dev/null || true)"
default_branch="$(git symbolic-ref --short -q refs/remotes/origin/HEAD 2> /dev/null || true)" default_branch="${default_branch#origin/}"
default_branch="${default_branch#origin/}" if [[ -n "$current_branch" && -n "$default_branch" ]]; then
if [[ "$current_branch" == "${default_branch:-main}" ]]; then if [[ "$current_branch" == "$default_branch" ]]; then
lock_advice="this is the default branch, so commit it or discard it deliberately." lock_advice="this is the default branch, so commit it or discard it deliberately."
else else
lock_advice="this is a feature branch, so discard it: git checkout -- apm.lock.yaml && apm install" lock_advice="this is a feature branch, so discard it: git checkout -- apm.lock.yaml && apm install"

View File

@@ -6,16 +6,29 @@ set -euo pipefail
# is the gate that holds the rule, since skill-size-check only checks presence # is the gate that holds the rule, since skill-size-check only checks presence
# and shape. # and shape.
# #
# Baseline: `git merge-base <main> <pushed commit>`, where <main> is origin/main # Baseline: `git merge-base --all <main> <pushed commit>`, where <main> is
# when it resolves and the local `main` branch otherwise. Readers install # origin/main when it resolves and the local `main` branch otherwise. Readers
# skills from main, so "changed" means changed relative to what main ships, not # install skills from main, so "changed" means changed relative to what main
# relative to the remote branch's current tip. Diffing from PRE_COMMIT_FROM_REF # ships, not relative to the remote branch's current tip. Diffing from
# would let the second push of a feature branch excuse a change the first push # PRE_COMMIT_FROM_REF would let the second push of a feature branch excuse a
# already carried unbumped. The check runs on every push whatever the target # change the first push already carried unbumped. The check runs on every push
# branch — nothing here reads PRE_COMMIT_REMOTE_BRANCH — so it also runs under # whatever the target branch — nothing here reads PRE_COMMIT_REMOTE_BRANCH — so
# a manual `pre-commit run --hook-stage pre-push` (against HEAD, since no # it also runs under a manual `pre-commit run --hook-stage pre-push` (against
# PRE_COMMIT_TO_REF is set). A missing bump is cheapest to fix on the branch, # HEAD, since no PRE_COMMIT_TO_REF is set). A missing bump is cheapest to fix on
# before review. # the branch, before review.
#
# `--all`, not the single base git would otherwise pick for it. A criss-cross
# history — main merges a branch while that branch merges a main commit — has
# TWO merge bases, and which one `git merge-base` prints is an implementation
# detail. Picking one made the verdict a coin flip: a skill byte-identical to
# main's tip was still reported "not above merge-base" whenever the losing base
# happened to be chosen, so an already-merged bump failed the push it should
# have passed. So a skill counts as CHANGED only when it differs from EVERY
# base — differing from none of them, or from only some, means one base already
# carries the pushed content — and a changed skill's version must exceed the
# version at every base it exists at. Both directions are conservative: the
# intersection cannot exempt a skill that genuinely changed since all of main's
# reachable history, and requiring every base keeps the ratchet.
# #
# Second baseline: the tip of that same <main> ref. A changed skill's pushed # Second baseline: the tip of that same <main> ref. A changed skill's pushed
# version must exceed its version there too (ADR-0022, second 2026-09-16 # version must exceed its version there too (ADR-0022, second 2026-09-16
@@ -134,38 +147,70 @@ if [[ -z "$MAIN_REF" ]]; then
exit 1 exit 1
fi fi
if ! BASELINE="$(git merge-base "$MAIN_REF" "$PUSHED_COMMIT" 2>/dev/null)"; then BASES=()
while IFS= read -r base; do
[[ -n "$base" ]] && BASES+=("$base")
done < <(git merge-base --all "$MAIN_REF" "$PUSHED_COMMIT" 2>/dev/null || true)
if [[ ${#BASES[@]} -eq 0 ]]; then
echo "FAIL: no merge-base between $MAIN_REF and $PUSHED_REF, so there is no baseline to compare skill versions against." >&2 echo "FAIL: no merge-base between $MAIN_REF and $PUSHED_REF, so there is no baseline to compare skill versions against." >&2
echo " Fix: ensure full history is available (e.g. git fetch --unshallow) and retry." >&2 echo " Fix: ensure full history is available (e.g. git fetch --unshallow) and retry." >&2
exit 1 exit 1
fi fi
if [[ "$MAIN_REF" == "main" && "$BASELINE" == "$PUSHED_COMMIT" ]]; then # A pushed commit that is an ancestor of <main> is itself the only merge-base,
echo "FAIL: origin/main does not resolve and $PUSHED_REF is already contained in local main, so local main cannot serve as an independent baseline — the diff would be empty by construction." >&2 # so this fires on exactly the case it always did.
echo " Fix: git fetch origin main and retry." >&2 if [[ "$MAIN_REF" == "main" ]]; then
exit 1 for base in ${BASES[@]+"${BASES[@]}"}; do
if [[ "$base" == "$PUSHED_COMMIT" ]]; then
echo "FAIL: origin/main does not resolve and $PUSHED_REF is already contained in local main, so local main cannot serve as an independent baseline — the diff would be empty by construction." >&2
echo " Fix: git fetch origin main and retry." >&2
exit 1
fi
done
fi fi
CHANGED_FILE="$(mktemp)" CHANGED_FILE="$(mktemp)"
trap 'rm -f "$CHANGED_FILE"' EXIT trap 'rm -f "$CHANGED_FILE"' EXIT
if ! git diff -z --no-renames --name-only "$BASELINE" "$PUSHED_COMMIT" -- plugins > "$CHANGED_FILE"; then
echo "FAIL: could not diff $BASELINE..$PUSHED_REF (see git error above)." >&2
exit 1
fi
SKILL_PATH_RE='^(plugins/[^/]+/\.apm/skills/[^/]+)/(.+)$' SKILL_PATH_RE='^(plugins/[^/]+/\.apm/skills/[^/]+)/(.+)$'
SKILL_DIRS=()
while IFS= read -r -d '' path; do # skill_dirs_at <base>: sets SKILL_DIRS_ONE to the skill directories differing
[[ "$path" =~ $SKILL_PATH_RE ]] || continue # between <base> and the pushed commit.
[[ "${BASH_REMATCH[2]}" == tests/* ]] && continue skill_dirs_at() {
dir="${BASH_REMATCH[1]}" local path dir existing seen
seen=false SKILL_DIRS_ONE=()
for existing in ${SKILL_DIRS[@]+"${SKILL_DIRS[@]}"}; do if ! git diff -z --no-renames --name-only "$1" "$PUSHED_COMMIT" -- plugins > "$CHANGED_FILE"; then
[[ "$existing" == "$dir" ]] && { seen=true; break; } echo "FAIL: could not diff $1..$PUSHED_REF (see git error above)." >&2
exit 1
fi
while IFS= read -r -d '' path; do
[[ "$path" =~ $SKILL_PATH_RE ]] || continue
[[ "${BASH_REMATCH[2]}" == tests/* ]] && continue
dir="${BASH_REMATCH[1]}"
seen=false
for existing in ${SKILL_DIRS_ONE[@]+"${SKILL_DIRS_ONE[@]}"}; do
[[ "$existing" == "$dir" ]] && { seen=true; break; }
done
$seen || SKILL_DIRS_ONE+=("$dir")
done < "$CHANGED_FILE"
}
# Intersected across every base: a skill matching any ONE base is already
# shipped by that base and has nothing left to bump.
skill_dirs_at "${BASES[0]}"
SKILL_DIRS=(${SKILL_DIRS_ONE[@]+"${SKILL_DIRS_ONE[@]}"})
for ((i = 1; i < ${#BASES[@]}; i++)); do
[[ ${#SKILL_DIRS[@]} -eq 0 ]] && break
skill_dirs_at "${BASES[i]}"
KEPT=()
for dir in ${SKILL_DIRS[@]+"${SKILL_DIRS[@]}"}; do
for existing in ${SKILL_DIRS_ONE[@]+"${SKILL_DIRS_ONE[@]}"}; do
[[ "$existing" == "$dir" ]] && { KEPT+=("$dir"); break; }
done
done done
$seen || SKILL_DIRS+=("$dir") SKILL_DIRS=(${KEPT[@]+"${KEPT[@]}"})
done < "$CHANGED_FILE" done
[[ ${#SKILL_DIRS[@]} -eq 0 ]] && exit 0 [[ ${#SKILL_DIRS[@]} -eq 0 ]] && exit 0
@@ -238,35 +283,64 @@ in_tree() {
OFFENDERS=() OFFENDERS=()
for dir in ${SKILL_DIRS[@]+"${SKILL_DIRS[@]}"}; do for dir in ${SKILL_DIRS[@]+"${SKILL_DIRS[@]}"}; do
at_base=false # Bases the skill exists at, in merge-base order, with the version read at
# each. Index-matched arrays rather than one map: bash 3.2 has no `declare -A`.
base_at=()
base_vers=()
first_base_ver=""
for base in ${BASES[@]+"${BASES[@]}"}; do
in_tree "$base" "$dir/SKILL.md" || continue
version_at "$base" "$dir/SKILL.md"
base_at+=("$base")
base_vers+=("$VERSION")
[[ -n "$first_base_ver" ]] || first_base_ver="$VERSION"
done
# When main has not moved since a merge-base, the tip IS that baseline and the
# skill is checked once.
at_tip=false at_tip=false
in_tree "$BASELINE" "$dir/SKILL.md" && at_base=true tip_is_base=false
# When main has not moved since the merge-base, the tip is the same baseline. for base in ${BASES[@]+"${BASES[@]}"}; do
[[ "$MAIN_TIP" != "$BASELINE" ]] && in_tree "$MAIN_TIP" "$dir/SKILL.md" && at_tip=true if [[ "$MAIN_TIP" == "$base" ]]; then
# Absent at both baselines: new, renamed-to, or merged-into. Exempt. tip_is_base=true
$at_base || $at_tip || continue break
fi
done
if [[ "$tip_is_base" == false ]] && in_tree "$MAIN_TIP" "$dir/SKILL.md"; then
at_tip=true
fi
# Absent at every baseline: new, renamed-to, or merged-into. Exempt.
[[ ${#base_at[@]} -gt 0 ]] || $at_tip || continue
# Directory absent at pushed commit: deleted or renamed-from. Exempt. # Directory absent at pushed commit: deleted or renamed-from. Exempt.
[[ "$(git cat-file -t "$PUSHED_COMMIT:$dir" 2>/dev/null)" == "tree" ]] || continue [[ "$(git cat-file -t "$PUSHED_COMMIT:$dir" 2>/dev/null)" == "tree" ]] || continue
base_ver=""
tip_ver="" tip_ver=""
if $at_base; then version_at "$BASELINE" "$dir/SKILL.md"; base_ver="$VERSION"; fi
if $at_tip; then version_at "$MAIN_TIP" "$dir/SKILL.md"; tip_ver="$VERSION"; fi if $at_tip; then version_at "$MAIN_TIP" "$dir/SKILL.md"; tip_ver="$VERSION"; fi
if ! in_tree "$PUSHED_COMMIT" "$dir/SKILL.md"; then if ! in_tree "$PUSHED_COMMIT" "$dir/SKILL.md"; then
OFFENDERS+=("$dir: SKILL.md missing at $PUSHED_REF (baseline: ${base_ver:-none})") OFFENDERS+=("$dir: SKILL.md missing at $PUSHED_REF (baseline: ${first_base_ver:-none})")
continue continue
fi fi
version_at "$PUSHED_COMMIT" "$dir/SKILL.md" version_at "$PUSHED_COMMIT" "$dir/SKILL.md"
cur_ver="$VERSION" cur_ver="$VERSION"
if [[ -z "$cur_ver" ]]; then if [[ -z "$cur_ver" ]]; then
OFFENDERS+=("$dir: metadata.version missing or not MAJOR.MINOR.PATCH at $PUSHED_REF (baseline: ${base_ver:-none})") OFFENDERS+=("$dir: metadata.version missing or not MAJOR.MINOR.PATCH at $PUSHED_REF (baseline: ${first_base_ver:-none})")
continue continue
fi fi
if [[ -n "$base_ver" ]] && ! semver_gt "$cur_ver" "$base_ver"; then # Named by sha only when there is more than one base to tell apart; a
OFFENDERS+=("$dir: $base_ver -> $cur_ver (not above merge-base)") # criss-cross history is the only case where "which merge-base" is a question
fi # the reader cannot answer from the branch alone.
for ((i = 0; i < ${#base_at[@]}; i++)); do
base_ver="${base_vers[i]}"
[[ -n "$base_ver" ]] || continue
semver_gt "$cur_ver" "$base_ver" && continue
if [[ ${#BASES[@]} -gt 1 ]]; then
OFFENDERS+=("$dir: $base_ver -> $cur_ver (not above merge-base ${base_at[i]})")
else
OFFENDERS+=("$dir: $base_ver -> $cur_ver (not above merge-base)")
fi
done
if [[ -n "$tip_ver" ]] && ! semver_gt "$cur_ver" "$tip_ver"; then if [[ -n "$tip_ver" ]] && ! semver_gt "$cur_ver" "$tip_ver"; then
OFFENDERS+=("$dir: $tip_ver -> $cur_ver (not above $MAIN_REF tip)") OFFENDERS+=("$dir: $tip_ver -> $cur_ver (not above $MAIN_REF tip)")
fi fi

View File

@@ -169,6 +169,67 @@ done < <(
| sort | sort
) )
# Discovering NOTHING is never a clean run, and it used to be the quietest
# possible pass: the loops below iterate zero times, nothing is printed between
# the bats block and the summary, and `Summary: 0 passed, 0 failed` exits 0 --
# under --strict too, because strictness only ever turned SKIPS into failures
# and there were no suites to skip. A wrong TEST_DIR, a mistyped `find` pattern,
# an exclusion that grew to swallow tests/, and a gutted checkout all land here.
# tests/run-bats.sh has carried this guard for its own .bats discovery; this is
# the same guard one file over, and the run-tests pre-push hook is the caller
# that needs it.
#
# Two checks, in the same order and for the same reasons as run-bats.sh's.
# First, the derived one: every test-*.sh in the git index must have been
# discovered. The direction matters -- a discovered file need NOT be tracked
# (work in progress is ordinary), and a file removed with `git rm` leaves the
# index, so a deliberate removal passes while an accidental disappearance
# fails. It only runs when SEARCH_ROOT is itself the git worktree root, which
# is what keeps it off the mktemp fixture trees in tests/test-run-tests.sh --
# those hold one or two test-*.sh files by design and git resolves no worktree
# for them. The same exclusions are reapplied to the index listing so both
# sides cover the same universe.
EXPECTED_SCRIPTS=()
GIT_TOPLEVEL="$(git -C "$SEARCH_ROOT" rev-parse --show-toplevel 2> /dev/null || true)"
if [[ -n "$GIT_TOPLEVEL" && "$GIT_TOPLEVEL" == "$SEARCH_ROOT" ]]; then
while IFS= read -r f; do
[[ -n "$f" ]] && EXPECTED_SCRIPTS+=("$SEARCH_ROOT/$f")
done < <(
git -C "$SEARCH_ROOT" ls-files -- 'test-*.sh' '*/test-*.sh' \
| grep -Ev '(^|/)\.claude/worktrees/|(^|/)apm_modules/|(^|/)\.claude/skills/' \
| sort || true
)
fi
if [[ ${#EXPECTED_SCRIPTS[@]} -gt 0 ]]; then
MISSING_SCRIPTS=()
for expected in ${EXPECTED_SCRIPTS[@]+"${EXPECTED_SCRIPTS[@]}"}; do
found=false
for actual in ${SCRIPTS[@]+"${SCRIPTS[@]}"}; do
if [[ "$actual" == "$expected" ]]; then
found=true
break
fi
done
[[ "$found" == true ]] || MISSING_SCRIPTS+=("${expected#"$SEARCH_ROOT"/}")
done
if [[ ${#MISSING_SCRIPTS[@]} -gt 0 ]]; then
echo "Error: ${#MISSING_SCRIPTS[@]} of ${#EXPECTED_SCRIPTS[@]} tracked test-*.sh file(s) were not discovered under $SEARCH_ROOT — they were deleted without being removed from the index, or the search path/exclusions above no longer reach them:" >&2
for m in ${MISSING_SCRIPTS[@]+"${MISSING_SCRIPTS[@]}"}; do
echo " $m" >&2
done
exit 1
fi
fi
# Second, unconditional and separate: a tree with nothing tracked (a tarball
# export, a fresh scaffold) still must not run on an empty set and call it
# green.
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
echo "Error: found 0 test-*.sh file(s) under $SEARCH_ROOT — the search path is wrong or the suite has been gutted" >&2
exit 1
fi
# Each test-*.sh is independent (fixtures live under its own mktemp dir, none # Each test-*.sh is independent (fixtures live under its own mktemp dir, none
# write back into the live repo tree -- verified before adding this), so they # write back into the live repo tree -- verified before adding this), so they
# run concurrently in fixed-size batches instead of one at a time. Dispatch and # run concurrently in fixed-size batches instead of one at a time. Dispatch and

View File

@@ -441,6 +441,231 @@ else
fail "validate.sh agent mode exited $SILENT_RC with output '${SILENT_OUT:-<empty>}' — the original defect was exit 0 and total silence on a blocking pre-push gate" fail "validate.sh agent mode exited $SILENT_RC with output '${SILENT_OUT:-<empty>}' — the original defect was exit 0 and total silence on a blocking pre-push gate"
fi fi
# ---------------------------------------------------------------------------
# 3. The REQUIRED-FIELD checks, folded in from the deleted `skill-frontmatter`
# ---------------------------------------------------------------------------
# Commit c8a7c9e retired the standalone `skill-frontmatter` hook and moved its
# two presence checks — `name` non-empty, `metadata.version` present and
# three-part semver — into skill-size-check.sh, beside the ADR-0020 gates. The
# hook's own suite went with it, and only the leading-zero shape was left
# covered (tests/test-skill-size-check.sh). Measured: mutating the
# missing-version ERROR to a no-op left every suite in the repo green. These
# cases are that behaviour pinned back down, on the same fixtures the deleted
# suite used.
#
# Probed against the HOOK alone, deliberately. validate.sh's skill mode has its
# own metadata.version check with its own wording, and its agent mode has none
# at all — ADR-0022 binds skills, not agents — so probe_all's "all three must
# agree" contract does not hold for this family and asserting it would be
# asserting something the ADRs contradict.
#
# The four grep defects the deleted suite named are kept as cases because the
# YAML-parsed implementation must not regress into any of them: a `metadata:`
# block quoted in the BODY, a `version:` under a following `source:` list, a
# deeper-indented `version:`, and the mirror image — a `version:` far down a
# long metadata block, which the old `-A10` grep reported MISSING.
# write_required <name> <frontmatter> [body] — a SKILL.md whose only interesting
# property is its frontmatter. The description and body sit well inside every
# ADR-0020 ceiling, so a finding here is the required-field check and nothing
# else; a fixture that also tripped a ceiling would satisfy "exits non-zero" for
# the wrong reason.
write_required() {
local name="$1" frontmatter="$2" body="${3:-Body text.}"
local dir="$TMPDIR_T/required/$name"
mkdir -p "$dir"
{
printf -- '---\n'
printf '%s\n' "$frontmatter"
printf -- '---\n\n'
printf '%s\n' "$body"
} > "$dir/SKILL.md"
printf '%s' "$dir/SKILL.md"
}
# write_required_raw <name> <whole-file> — for the shapes that must NOT have a
# closing marker written for them.
write_required_raw() {
local name="$1"
local dir="$TMPDIR_T/required/$name"
mkdir -p "$dir"
printf '%s' "$2" > "$dir/SKILL.md"
printf '%s' "$dir/SKILL.md"
}
REQ_DESC='Use when probing the required-field checks. Do not use for anything else.'
# require_finding <label> <file> <needle> — non-zero exit AND the named message.
# The needle is the message, not the exit code: the mutation this case exists to
# catch turns the ERROR into a no-op, and a file that also failed some other gate
# would still exit non-zero with the check gone.
require_finding() {
local label="$1" file="$2" needle="$3" out status=0
set +e
out="$(bash "$HOOK" "$file" 2>&1)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
fail "$label — the hook exited 0: ${out:-<no output>}"
elif [[ "$out" != *"$needle"* ]]; then
fail "$label — the hook failed but never said '$needle': $out"
else
pass "$label"
fi
}
# require_clean <label> <file> — exits 0 with no finding at all.
require_clean() {
local label="$1" file="$2" out status=0
set +e
out="$(bash "$HOOK" "$file" 2>&1)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
pass "$label"
else
fail "$label — expected exit 0, got $status: ${out:-<no output>}"
fi
}
echo ""
echo "--- control: a SKILL.md carrying both required fields passes ---"
# Without this, every case below could be passing because the fixture generator
# is broken rather than because the checks fire.
require_clean "a well-formed name + metadata.version passes" \
"$(write_required valid "name: valid
description: $REQ_DESC
metadata:
version: \"1.0.0\"")"
echo ""
echo "--- metadata.version missing, in each of the shapes that used to satisfy the old grep ---"
require_finding "no metadata block at all is reported missing" \
"$(write_required no-metadata "name: no-metadata
description: $REQ_DESC")" \
"metadata.version field is missing"
require_finding "a metadata block with other keys but no version is reported missing" \
"$(write_required metadata-no-version "name: metadata-no-version
description: $REQ_DESC
metadata:
author: someone")" \
"metadata.version field is missing"
# `version:` with no value parses to None, which is absent, not malformed —
# reporting it as a bad VALUE would send the author looking for a typo in a
# value that is not there.
require_finding "a valueless 'version:' is reported missing, not malformed" \
"$(write_required empty-version "name: empty-version
description: $REQ_DESC
metadata:
version:")" \
"metadata.version field is missing"
# skill-author's own docs quote a metadata block verbatim; under the old
# whole-file grep that quotation satisfied the check for the file quoting it.
require_finding "a metadata block quoted in the BODY does not satisfy the check" \
"$(write_required fenced-metadata "name: fenced-metadata
description: $REQ_DESC" '# Fenced
Skills declare their version like this:
```yaml
metadata:
version: "1.0.0"
```')" \
"metadata.version field is missing"
# `-A10` ran ten lines past `metadata:` regardless of where the block ended, and
# write-docs and research both carry a `source:` list immediately after it.
require_finding "a version: belonging to a following source[] does not satisfy the check" \
"$(write_required source-list "name: source-list
description: $REQ_DESC
metadata:
author: someone
source:
- name: upstream
version: \"2.3.4\"")" \
"metadata.version field is missing"
# `grep -q \" version:\"` was an unanchored substring match, so any indentation
# of two spaces or more matched.
require_finding "a four-space-indented version: one level deeper does not satisfy the check" \
"$(write_required deep-indent "name: deep-indent
description: $REQ_DESC
metadata:
provenance:
version: \"1.0.0\"")" \
"metadata.version field is missing"
# The mirror image, and the reason this one asserts a PASS: the old grep's
# ten-line window reported a real version missing once the block grew past it.
require_clean "a version: thirteen lines into the metadata block is found" \
"$(write_required long-metadata "name: long-metadata
description: $REQ_DESC
metadata:
a: 1
b: 2
c: 3
d: 4
e: 5
f: 6
g: 7
h: 8
i: 9
j: 10
k: 11
version: \"1.0.0\"")"
echo ""
echo "--- present is not well formed: a non-semver metadata.version is its own finding ---"
# plugins/bin/.apm/skills/write-docs/SKILL.md carried `version: "1.0"` through a
# whole PR under a presence-only check: present, well-nested, and not a version.
# The value is quoted back so the author does not have to guess which key.
require_finding "'1.0' is rejected as malformed and the message quotes it" \
"$(write_required two-part "name: two-part
description: $REQ_DESC
metadata:
version: \"1.0\"")" \
"metadata.version is malformed ('1.0')"
require_finding "'latest' is rejected as malformed and the message quotes it" \
"$(write_required word-version "name: word-version
description: $REQ_DESC
metadata:
version: latest")" \
"metadata.version is malformed ('latest')"
echo ""
echo "--- the name field is required and must not be empty ---"
require_finding "an absent name is reported" \
"$(write_required no-name "description: $REQ_DESC
metadata:
version: \"1.0.0\"")" \
"name field is missing or empty"
require_finding "an empty name is reported" \
"$(write_required empty-name "name: \"\"
description: $REQ_DESC
metadata:
version: \"1.0.0\"")" \
"name field is missing or empty"
echo ""
echo "--- a file whose frontmatter block cannot be read reports THAT, not a missing field ---"
# The required-field checks run downstream of the frontmatter match, so a file
# with no readable block must land on the parse error rather than being reported
# as a skill that merely forgot its version — and must never report green.
require_finding "an unterminated frontmatter block is a parse error" \
"$(write_required_raw unterminated "---
name: unterminated
description: $REQ_DESC
metadata:
version: \"1.0.0\"
Body text.
")" \
"no parseable YAML frontmatter block"
require_finding "an empty '---/---' block is a parse error" \
"$(write_required_raw empty-block "---
---
Body text.
")" \
"no parseable YAML frontmatter block"
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] [[ $FAIL -eq 0 ]]

View File

@@ -150,6 +150,23 @@ if command -v git > /dev/null 2>&1; then
commit -q --allow-empty -m init commit -q --allow-empty -m init
touch "$REPO/apm.lock.yaml" touch "$REPO/apm.lock.yaml"
# origin/HEAD is unset here — git writes it on clone and `git remote add` does
# not — so the hook cannot know what the default branch is and must not guess.
# It used to assume `main`, which is why the `master` case further down was a
# live defect.
out="$(run_hook_in "$REPO" "$REPO")"
advice="$(advice_of "$out")"
grep -q "commit it or discard it deliberately" <<< "$advice" \
&& pass "with origin/HEAD unset, keeps the neutral lock advice" \
|| fail "with origin/HEAD unset the advice should stay neutral: $advice"
# Needles are the two DECISION phrases, not the bare words: the fixed prefix
# of every notice already says "behind the remote default branch".
grep -qE "this is (the default|a feature) branch, so" <<< "$advice" \
&& fail "with origin/HEAD unset the hook must not claim to know which branch this is: $advice" \
|| pass "with origin/HEAD unset, claims nothing about which branch this is"
git -C "$REPO" update-ref refs/remotes/origin/main HEAD
git -C "$REPO" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
out="$(run_hook_in "$REPO" "$REPO")" out="$(run_hook_in "$REPO" "$REPO")"
grep -q "default branch, so commit it or discard it deliberately" <<< "$(advice_of "$out")" \ grep -q "default branch, so commit it or discard it deliberately" <<< "$(advice_of "$out")" \
&& pass "on main, says to commit or discard the lock deliberately" \ && pass "on main, says to commit or discard the lock deliberately" \
@@ -174,6 +191,26 @@ if command -v git > /dev/null 2>&1; then
grep -q "default branch, so commit it" <<< "$(advice_of "$out")" \ grep -q "default branch, so commit it" <<< "$(advice_of "$out")" \
&& pass "reads the default branch from origin/HEAD when it is set" \ && pass "reads the default branch from origin/HEAD when it is set" \
|| fail "should treat origin/HEAD's branch as the default: $(advice_of "$out")" || fail "should treat origin/HEAD's branch as the default: $(advice_of "$out")"
# The regression the `${default_branch:-main}` fallback caused: a repo whose
# default branch is `master`, with origin/HEAD unset (no clone wrote it), was
# standing on its DEFAULT branch and was told to discard the lock as feature
# churn. Assuming `main` is the only way to reach that verdict, so the case is
# pinned on the branch name that makes the assumption wrong.
MASTER_REPO="$WORK/master-repo"
mkdir -p "$MASTER_REPO"
git -C "$MASTER_REPO" init -q -b master
git -C "$MASTER_REPO" -c user.email=probe@example.invalid -c user.name=probe \
commit -q --allow-empty -m init
touch "$MASTER_REPO/apm.lock.yaml"
out="$(run_hook_in "$MASTER_REPO" "$MASTER_REPO")"
advice="$(advice_of "$out")"
grep -qF "feature branch, so discard it" <<< "$advice" \
&& fail "on master with origin/HEAD unset the hook assumed main and told the reader to discard a real lock update: $advice" \
|| pass "on master with origin/HEAD unset, does not misread the default branch as a feature branch"
grep -q "commit it or discard it deliberately" <<< "$advice" \
&& pass "on master with origin/HEAD unset, falls back to the neutral lock advice" \
|| fail "on master with origin/HEAD unset the advice should be neutral: $advice"
else else
echo " (git not on PATH — branch-specific advice cases not run)" echo " (git not on PATH — branch-specific advice cases not run)"
fi fi

View File

@@ -703,6 +703,74 @@ else
pass "a root under .claude/worktrees/ runs its own suites and skips nested worktrees" pass "a root under .claude/worktrees/ runs its own suites and skips nested worktrees"
fi fi
# --- 13. Discovering ZERO test-*.sh files is a hard error, not a green run ---
# The quietest vacuous pass in the dispatcher, and the one --strict did not
# reach: strictness only ever turned SKIPS into failures, so with no suites to
# skip there was nothing for it to act on. The loops iterated zero times, the
# run printed `Summary: 0 passed, 0 skipped, 0 failed` and exited 0 -- from the
# run-tests pre-push hook, which prints nothing for a passing hook, that is
# indistinguishable from every suite passing. A wrong TEST_DIR, a mistyped find
# pattern, an exclusion that grew to swallow tests/, and a gutted checkout all
# land here. tests/run-bats.sh has guarded its own discovery this way since it
# hit the same hole; this is the sibling.
#
# The bats runner is the healthy stub, so the only thing wrong with the fixture
# is that it has no case scripts -- a failure here cannot be the bats leg.
echo ""
echo "--- discovering zero test-*.sh files fails the run and names the search root ---"
DIR13="$(make_fake_repo)"
FIXTURES+=("$DIR13")
install_healthy_bats_runner "$DIR13"
run_fake "$DIR13"
if [[ $FAKE_RC -eq 0 ]]; then
fail "an empty search root exited 0 — a run that verified nothing reports the same as a run that verified everything: $FAKE_OUT"
elif ! grep -q "found 0 test-\*\.sh file(s)" <<< "$FAKE_OUT"; then
fail "an empty search root failed without saying that nothing was discovered: $FAKE_OUT"
elif ! grep -qF "$DIR13/cases" <<< "$FAKE_OUT"; then
fail "the zero-discovery error did not name the root it searched, which is the one fact needed to fix it: $FAKE_OUT"
elif grep -q "^=== Summary:" <<< "$FAKE_OUT"; then
fail "an empty search root still printed a summary line, so a reader scanning for the verdict sees a green one: $FAKE_OUT"
else
pass "zero discovered test-*.sh files fails the run, names the search root, and prints no summary"
fi
# And under --strict too. Asserted separately because the ONLY lever --strict
# had was the skip list, so "it fails now" and "it fails under the gate's own
# invocation" were genuinely different questions here.
echo ""
echo "--- ... and under --strict, which previously had no lever on this at all ---"
DIR13B="$(make_fake_repo)"
FIXTURES+=("$DIR13B")
install_healthy_bats_runner "$DIR13B"
run_fake "$DIR13B" --strict
if [[ $FAKE_RC -eq 0 ]]; then
fail "an empty search root exited 0 under --strict, the invocation the run-tests pre-push hook uses: $FAKE_OUT"
elif grep -q "found 0 test-\*\.sh file(s)" <<< "$FAKE_OUT"; then
pass "--strict also fails a run that discovered nothing"
else
fail "--strict failed an empty search root for some other reason: $FAKE_OUT"
fi
# The control: one discovered case is enough to get past the guard, so the two
# cases above fail on the count and not on something else the fixture lacks.
echo ""
echo "--- ... while a single discovered case still passes ---"
DIR13C="$(make_fake_repo)"
FIXTURES+=("$DIR13C")
install_healthy_bats_runner "$DIR13C"
add_case "$DIR13C" test-one.sh <<'EOF'
#!/usr/bin/env bash
echo "one case ran"
EOF
run_fake "$DIR13C"
if [[ $FAKE_RC -ne 0 ]]; then
fail "a fixture with exactly one case failed: $FAKE_OUT"
elif grep -q "found 0 test-\*\.sh file(s)" <<< "$FAKE_OUT"; then
fail "the zero-discovery guard fired on a root that has one case script: $FAKE_OUT"
else
pass "one discovered case script is enough to get past the zero-discovery guard"
fi
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] [[ $FAIL -eq 0 ]]

View File

@@ -588,6 +588,51 @@ expect_fail "unbumped skill with leading blank lines is held to its baseline ver
write_skill "$F" demo alpha 'version: "1.0.1"' "lead body 2"; blank_lead "$F" alpha; commit "$F" write_skill "$F" demo alpha 'version: "1.0.1"' "lead body 2"; blank_lead "$F" alpha; commit "$F"
expect_pass "bumped skill with leading blank lines passes" "$F" expect_pass "bumped skill with leading blank lines passes" "$F"
echo ""
echo "--- 40. a criss-cross history is judged against EVERY merge-base ---"
# Two merge bases, and which one plain `git merge-base` prints is git's choice,
# not a property of the history. The gate used to take that single answer, so
# the verdict turned on it: here `git diff main feature -- plugins` is EMPTY
# (main already carries the bump, via its merge of the feature branch) and the
# push still failed with "not above main tip", because the base git picked was
# the one that predates the bump. `--all` plus the intersection rule makes the
# answer the same whichever base git would have named.
#
# C0 alpha 1.0.0
# +-- feature: F1 bumps alpha to 1.0.1
# +-- main: M1 unrelated, then M2 merges F1
# feature: F2 merges M1 -> merge bases {M1, F1}
F="$(mktemp -d)"; CLEANUP_DIRS+=("$F")
(cd "$F" && git init -q -b main && git config user.email t@t.t && git config user.name t)
write_skill "$F" demo alpha 'version: "1.0.0"'; commit "$F" C0
(cd "$F" && git checkout -q -b feature)
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F" F1
F1_SHA="$(cd "$F" && git rev-parse HEAD)"
(cd "$F" && git checkout -q main)
echo unrelated > "$F/m1.txt"; commit "$F" M1
M1_SHA="$(cd "$F" && git rev-parse HEAD)"
(cd "$F" && git merge -q --no-edit "$F1_SHA" -m M2 > /dev/null)
(cd "$F" && git checkout -q feature && git merge -q --no-edit "$M1_SHA" -m F2 > /dev/null)
BASES40="$(cd "$F" && git merge-base --all main feature | sort)"
if [[ "$(printf '%s\n' "$BASES40" | wc -l)" -eq 2 ]]; then
pass "fixture check: the history really does have two merge bases"
else
fail "fixture check: expected two merge bases, got: $BASES40"
fi
if [[ -z "$(cd "$F" && git diff main feature -- plugins)" ]]; then
pass "fixture check: nothing under plugins/ differs between main and the branch"
else
fail "fixture check: plugins/ differs between main and the branch, so this is not the case under test"
fi
expect_pass "a skill identical to main's tip passes whichever merge-base git would pick" "$F"
# And the ratchet still holds on the same shape: a further edit with no bump
# differs from BOTH bases, so it is not excused by the criss-cross.
write_skill "$F" demo alpha 'version: "1.0.1"' "later body"; commit "$F" F3
expect_fail "an unbumped edit on a criss-cross branch still fails, naming the baseline sha" \
"alpha: 1\.0\.1 -> 1\.0\.1 \(not above merge-base [0-9a-f]{40}\)" "$F"
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] [[ $FAIL -eq 0 ]]

View File

@@ -1783,7 +1783,14 @@ fi
# merge took out with the script, and cases 28-30 cannot backstop it. They key # merge took out with the script, and cases 28-30 cannot backstop it. They key
# on `Kyberforge.VagueWording` and `KyberforgeCopilot.ProactivePhrase`, so # on `Kyberforge.VagueWording` and `KyberforgeCopilot.ProactivePhrase`, so
# DescriptionOpener, PaddingPhrase, SentenceOpenerThereIs and CompositionNote # DescriptionOpener, PaddingPhrase, SentenceOpenerThereIs and CompositionNote
# can each be retired underneath a passing probe. # are invisible to them.
#
# Which is a statement about the LEVEL of each rule, and only that. It used to
# read as though those four rules were uncovered outright, and they were: case
# 35 at the end of this file is what closed that, enumerating the style
# directories at run time and demanding an alert from every rule it finds. The
# two cases are complementary and neither subsumes the other — 35 proves a rule
# still matches text, this one proves the match is still blocking.
# #
# The original's comments, verbatim: # The original's comments, verbatim:
# #
@@ -2364,6 +2371,124 @@ EOF_MUT34
fi fi
fi fi
# --- 35. Every shipped rule actually fires on a fixture ---------------------
#
# The last coverage class the deleted scripts/check-vale-style-sync.sh and its
# consumer suite took with them (ADR-0025). Cases 28-31 keep a rule LOADED, at
# `error`, and in scope; none of them asks whether the rule still MATCHES
# anything. Measured: rewriting CompositionNote.yml's tokens so they match no
# text left this suite at 63/63 passed — the rule shipped, was loaded, was
# blocking, and was inert.
#
# The rule list is discovered from the style directories at run time, never
# hardcoded, and a discovered rule with no fixture row is a FAILURE rather than
# a silent skip. That direction is the one that decays: a hardcoded list lets
# rule #7 ship uncovered, and a fixture table read as "check the rows I have"
# does exactly the same.
VALE_ASSETS35="$FACTORY_AUDIT/assets/vale"
# `<Style>.<Rule>` for every rule file under styles/Kyberforge*/, which is how
# vale itself names an alert.
discovered_rules35() {
local dir rule style name
for dir in "$VALE_ASSETS35"/styles/Kyberforge*/; do
[[ -d "$dir" ]] || continue
style="${dir%/}"
style="${style##*/}"
for rule in "$dir"*.yml; do
[[ -f "$rule" ]] || continue
name="${rule##*/}"
printf '%s.%s\n' "$style" "${name%.yml}"
done
done | sort
}
# `<Style>.<Rule>|<path>|<description>|<body>`. The path decides which [glob]
# section of .vale.ini applies, so KyberforgeCopilot's row has to be an
# .agent.md file — that style is loaded nowhere else (ADR-0013). Each fixture
# carries exactly the one trigger its rule is about; the rest of the text is
# deliberately clean, so an alert for the wrong rule cannot satisfy the row.
RULE_FIXTURES35="$(
cat << 'EOF_FIX35'
Kyberforge.CompositionNote|composition/SKILL.md|Use when the caller wants a cross-cutting probe. Do not use for anything else.|Body text.
Kyberforge.DescriptionOpener|opener/SKILL.md|This is the description opener under test. Do not use for anything else.|Body text.
Kyberforge.PaddingPhrase|padding/SKILL.md|Use when the caller wants a probe. Do not use for anything else.|See references for more info.
Kyberforge.SentenceOpenerThereIs|sentence-opener/SKILL.md|Use when the caller wants a probe. Do not use for anything else.|There is a defect here.
Kyberforge.VagueWording|vague/SKILL.md|Use when the caller helps with a probe. Do not use for anything else.|Body text.
KyberforgeCopilot.ProactivePhrase|proactive/probe.agent.md|Use when the caller wants a probe. Use proactively.|Body text.
EOF_FIX35
)"
echo ""
echo "--- every shipped Vale rule has a fixture, and every fixture names a shipped rule (no vale needed) ---"
DISCOVERED35="$(discovered_rules35)"
UNCOVERED35=""
STALE_FIXTURES35=""
while IFS= read -r RULE35; do
[[ -n "$RULE35" ]] || continue
FOUND35=false
while IFS='|' read -r FID35 _; do
[[ "$FID35" == "$RULE35" ]] && { FOUND35=true; break; }
done <<EOF_COV35
$RULE_FIXTURES35
EOF_COV35
[[ "$FOUND35" == true ]] || UNCOVERED35+="$RULE35 "
done <<EOF_RULES35
$DISCOVERED35
EOF_RULES35
while IFS='|' read -r FID35 _; do
[[ -n "$FID35" ]] || continue
grep -qxF "$FID35" <<< "$DISCOVERED35" || STALE_FIXTURES35+="$FID35 "
done <<EOF_STALE35
$RULE_FIXTURES35
EOF_STALE35
if [[ -z "$DISCOVERED35" ]]; then
fail "no rule files were found under $VALE_ASSETS35/styles/Kyberforge*/ — either the styles were gutted or this discovery no longer reaches them, and every assertion below would be vacuous"
elif [[ -n "$UNCOVERED35" ]]; then
fail "shipped Vale rule(s) have no fixture row, so nothing proves they still match anything: $UNCOVERED35"
elif [[ -n "$STALE_FIXTURES35" ]]; then
fail "fixture row(s) name a rule that no longer ships, so those rows prove nothing about the live styles: $STALE_FIXTURES35"
else
pass "all $(printf '%s\n' "$DISCOVERED35" | wc -l | tr -d ' ') shipped rule(s) have a fixture row, and every row names a live rule"
fi
if [[ "$VALE_READY" == true ]]; then
echo ""
echo "--- and each of those fixtures actually raises its own rule's alert ---"
TREE35="$(mktemp -d)"
new_fixture "$TREE35"
INERT35=""
while IFS='|' read -r RID35 RPATH35 RDESC35 RBODY35; do
[[ -n "$RID35" ]] || continue
mkdir -p "$TREE35/$(dirname "$RPATH35")"
{
echo "---"
echo "name: probe"
echo "description: $RDESC35"
echo "---"
echo ""
echo "$RBODY35"
} > "$TREE35/$RPATH35"
# vale exits non-zero merely for HAVING alerts, which is the expected
# outcome for every row here, hence the `|| true`.
REPORT35="$( { (cd "$TREE35" && vale --config "$VALE_ASSETS35/.vale.ini" "$RPATH35" 2>&1) | sed -E 's/\x1b\[[0-9;]*m//g'; } || true)"
if ! grep -qE 'in [0-9]+ files?\.' <<< "$REPORT35"; then
INERT35+="[$RID35: vale printed no summary line for $RPATH35, so it did not run: ${REPORT35:-<no output>}] "
elif ! grep -qF "$RID35" <<< "$REPORT35"; then
INERT35+="[$RID35 raised no alert on its own fixture: ${REPORT35:-<no output>}] "
fi
done <<EOF_FIRE35
$RULE_FIXTURES35
EOF_FIRE35
if [[ -n "$INERT35" ]]; then
fail "a shipped rule matched nothing on the fixture written for it — it is loaded and blocking but inert, which is indistinguishable from a passing file: $INERT35"
else
pass "every shipped Vale rule raises its own alert on the fixture written for it"
fi
fi
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1 [[ $FAIL -eq 0 ]] || exit 1