diff --git a/CLAUDE.md b/CLAUDE.md index d3b5942..57d5338 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,7 +161,7 @@ make ui-test # Vitest component/store suite in a real browser (no app) make ui-visual # Same, including toMatchScreenshot comparisons make ui-setup # Install the Vitest provider's own Chromium (once) make bindings-check # Fail if frontend/bindings is stale vs the Go bindings -make skill-check # Fail if .pi/ documents a make target that doesn't exist +make skill-check # Fail if a doc names a make target that doesn't exist make commit-check # Fail if a commit subject is not a Conventional Commit make lint # golangci-lint v2 (strict), all three build configurations make test # All tests with race detector, all three build configurations diff --git a/Makefile b/Makefile index 75cba25..4560106 100644 --- a/Makefile +++ b/Makefile @@ -192,7 +192,7 @@ css-check: ## Fail on a css`` literal ended early by a backtick, or a nested rul # Every command in them is a make target on purpose, so this is # checkable. It also asserts AGENTS.md is a symlink to CLAUDE.md, so the # two harnesses cannot drift onto two descriptions of one project. -skill-check: ## Fail if the agent docs name a missing make target, or AGENTS.md is not a symlink +skill-check: ## Fail if the docs name a missing make target, or AGENTS.md is not a symlink @./scripts/skill-check.sh # Conventional Commits, which CLAUDE.md claimed CI enforced for a long diff --git a/lefthook.yml b/lefthook.yml index bbc42ce..170d57a 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -38,10 +38,14 @@ pre-commit: glob: "*.go" run: ./scripts/bindings-check.sh - # .pi/ documents make targets; a stale one sends an agent off a - # cliff with total confidence. Instant. + # The docs document make targets; a stale one sends an agent — or a + # contributor reading CONTRIBUTING.md — off a cliff with total + # confidence. The glob is the script's own scanned set, because a + # hook that does not fire on a file the check reads is the drift the + # check exists to prevent: it was `{Makefile,.pi/**/*.md}` while the + # script already read CLAUDE.md. Instant. skill-check: - glob: "{Makefile,.pi/**/*.md}" + glob: "{Makefile,.pi/**/*.md,AGENTS.md,CLAUDE.md,README.md,CONTRIBUTING.md}" run: ./scripts/skill-check.sh frontend-typecheck: diff --git a/scripts/skill-check.sh b/scripts/skill-check.sh index 56930a9..08181bb 100755 --- a/scripts/skill-check.sh +++ b/scripts/skill-check.sh @@ -14,6 +14,11 @@ # missing: CLAUDE.md names 27 targets and nothing verified one of them, # so the file the agents trust most was the file least checked. # +# README.md and CONTRIBUTING.md are in it too, and the header sentence +# above is why: a person who has *not* read the Makefile goes looking in +# the contributor-facing doc, so a renamed target sends them off the +# same cliff it sends an agent off. CONTRIBUTING.md names 21 targets. +# # **AGENTS.md is a symlink to CLAUDE.md.** This repo is worked on by # two agent harnesses that read different files by convention — Claude # Code reads CLAUDE.md, others read AGENTS.md — and two harnesses @@ -43,7 +48,19 @@ if [ -e AGENTS.md ] || [ -L AGENTS.md ]; then fi fi -[ -d .pi ] || exit 0 +# The scan is over the docs that are actually there: a checkout without +# .pi/ still has README.md and CONTRIBUTING.md to check, and gating the +# whole run on .pi/ would have made the human-facing half conditional on +# the agent-facing one. This list is used twice — once to read the +# mentions out and once to say which file a missing target came from — +# because a second list is a second thing to forget. +# `ls` exits non-zero when *any* of its arguments is missing while still +# printing the ones that are there, and under `set -e` that would sink +# the assignment rather than scanning what exists, so swallow it. +docs="$({ find .pi -name '*.md' 2>/dev/null + ls CLAUDE.md README.md CONTRIBUTING.md 2>/dev/null || true; })" + +[ -n "$docs" ] || exit 0 # `make -pq` prints the database including every rule, without running # anything. It exits non-zero when a target is out of date, and under @@ -68,7 +85,7 @@ targets="$({ make -pqRr 2>/dev/null || true; } | # AGENTS.md is deliberately not in this list: it is a symlink to # CLAUDE.md, asserted above, so scanning it would report every failure # twice under two names. -mentioned="$({ find .pi -name '*.md' 2>/dev/null; echo CLAUDE.md; } | +mentioned="$(printf '%s\n' "$docs" | xargs awk ' FNR == 1 { fence = 0 } /^```/ { fence = !fence; next } @@ -93,10 +110,10 @@ for t in $mentioned; do done if [ -n "$missing" ]; then - echo "skill-check: the agent docs name make targets that do not exist:" >&2 + echo "skill-check: the docs name make targets that do not exist:" >&2 for t in $missing; do echo " make $t" >&2 - grep -rln "make $t" .pi CLAUDE.md --include='*.md' | sed 's/^/ /' >&2 + printf '%s\n' "$docs" | xargs grep -ln "make $t" | sed 's/^/ /' >&2 done echo "Fix the docs, or restore the target." >&2 exit 1