feat(skills): declare which surfaces each skill ships to
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped

The repo distributes three ways — the Claude Code plugin, the symlinked
library for Crush/Pi/OpenCode, and claude.ai, which has no fetch path at all —
and treated every skill as belonging to all of them. claude.ai is not a fourth
host to mirror: gitea-ci, playwright and web-shot have no shell and no cluster
there, so a flat mirror installs skills that can never fire and degrades
selection for the ones that can.

Every SKILL.md now carries `surfaces:`, defaulting to [claude-code, library];
dream, own-the-task and grill-me add `chat`, matching what is actually
installed on claude.ai today. Claude Code accepts the extra key —
`claude plugin validate` passes and the skills still load.

bin/check-surfaces validates values, not just presence, because the failure
mode here is silent: a wrong surface removes a skill from a harness rather than
erroring. install.sh links only skills whose surfaces include `library`, and a
source that declares nothing at all is treated as foreign and linked as-is —
Anthropic's official set has no reason to know this convention. doctor's
missing-link check skips non-library skills for the same reason.

Refs #29
Bump-Type: minor
This commit is contained in:
2026-08-29 10:09:52 +02:00
parent 15737321ac
commit 90a58f0b9e
49 changed files with 214 additions and 1 deletions
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# Tests for bin/check-surfaces.
#
# The repo ships to three surfaces — the Claude Code plugin, the symlinked
# library (Crush/Pi/OpenCode) and claude.ai, which has no fetch path at all —
# and used to treat every skill as belonging to all of them. gitea-ci,
# playwright and web-shot cannot fire in a chat surface; a flat mirror installs
# skills that can never trigger and degrades selection for the ones that can
# (#29).
#
# The risk this gate exists for: a wrong `surfaces:` value silently removes a
# skill from a harness rather than erroring. So presence is not enough — values
# are validated against the allowed set.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/bin/check-surfaces"
pass=0
fail=0
WORK=${TMPDIR:-/tmp}/check_surfaces_test.$$
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT INT TERM
# fixture <name> <surfaces-line-or-empty> — a checkout holding one skill.
fixture() {
_f="$WORK/$1"
mkdir -p "$_f/skills/alpha"
if [ -n "$2" ]; then
printf -- '---\nname: alpha\n%s\ndescription: a\n---\n' "$2" > "$_f/skills/alpha/SKILL.md"
else
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
fi
printf '%s' "$_f"
}
# expect <rc> <label> <fixture> [needle]
expect() {
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
if [ "$_rc" -ne "$_want" ]; then
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
fi
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
fail=$((fail + 1)); printf ' FAIL %s (rc ok, report never says %s)\n%s\n' "$_label" "$_needle" "$_out"; return
fi
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
}
expect 0 'the default pair is accepted' "$(fixture ok 'surfaces: [claude-code, library]')"
expect 0 'all three surfaces are accepted' "$(fixture three 'surfaces: [claude-code, library, chat]')"
expect 0 'a single surface is accepted' "$(fixture one 'surfaces: [chat]')"
expect 1 'a missing surfaces field fails' "$(fixture missing '')" 'alpha'
expect 1 'an unknown surface value fails' "$(fixture bogus 'surfaces: [claude-code, desktop]')" 'desktop'
expect 1 'an empty surfaces list fails' "$(fixture empty 'surfaces: []')" 'alpha'
printf '\ncheck_surfaces: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]