#!/usr/bin/env bash # # Every command in the agent-facing docs is a `make` target on purpose: # the Makefile is the source of truth for *how* to invoke something, and # the docs only decide *which* and *in what order*. This check keeps # that honest — a renamed or deleted target turns into a failing commit # rather than into an agent confidently running a command that no longer # exists. # # It checks two things. Usage: scripts/skill-check.sh # # **Every `make ` named in an agent-facing doc exists.** The # scanned set is `.pi/` *and* CLAUDE.md, which is the half that was # missing: CLAUDE.md names 27 targets and nothing verified one of them, # so the file the agents trust most was the file least checked. # # **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 # reading two descriptions of one project is how they come to hold # different beliefs about it. A symlink makes that impossible by # construction; a *copy* would pass every other check in this repo while # silently drifting, which is exactly the failure being prevented, so # the symlink itself is asserted rather than its contents compared. set -euo pipefail cd "$(dirname "$0")/.." # The symlink half runs even without .pi/, since it is not about .pi/. if [ -e AGENTS.md ] || [ -L AGENTS.md ]; then if [ ! -L AGENTS.md ]; then echo "skill-check: AGENTS.md is a regular file, not a symlink to CLAUDE.md." >&2 echo " Two harnesses would read two descriptions of one project." >&2 echo " Fix: rm AGENTS.md && ln -s CLAUDE.md AGENTS.md" >&2 exit 1 fi target="$(readlink AGENTS.md)" if [ "$target" != "CLAUDE.md" ]; then echo "skill-check: AGENTS.md points at '$target', expected CLAUDE.md." >&2 exit 1 fi fi [ -d .pi ] || 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 # `pipefail` that would sink the whole assignment, so swallow it. targets="$({ make -pqRr 2>/dev/null || true; } | awk '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ {sub(/:.*/, "", $0); print}' | sort -u)" # A mention counts only when it is code: backticked (`make ui-test`) # anywhere, or at the start of a line **inside a fenced block**. Bare # prose is not scanned, because English says things like "a renamed make # target". # # The fence is why this is awk rather than one grep. Line-start alone is # not evidence of code in a file that is mostly hard-wrapped prose: the # sentence "Two green branches do not / make a green merge" wrapped onto # a line beginning `make a`, and the check duly failed on a target called # `a`. Inside a fence it is code; outside one it is a sentence that # happened to break there, and a check that fails on reflow gets # disabled rather than fixed. # # 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; } | xargs awk ' FNR == 1 { fence = 0 } /^```/ { fence = !fence; next } { rest = $0 while (match(rest, /`make [a-z][a-z0-9-]*/)) { print substr(rest, RSTART + 6, RLENGTH - 6) rest = substr(rest, RSTART + RLENGTH) } if (fence && match($0, /^make [a-z][a-z0-9-]*/)) { print substr($0, 6, RLENGTH - 5) } } ' | sort -u)" missing="" for t in $mentioned; do if ! printf '%s\n' "$targets" | grep -qx -- "$t"; then missing="$missing $t" fi done if [ -n "$missing" ]; then echo "skill-check: the agent 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 done echo "Fix the docs, or restore the target." >&2 exit 1 fi echo "skill-check: $(printf '%s\n' "$mentioned" | wc -w) documented make targets, all present"