#!/usr/bin/env bash # test-skill-triggers.sh — Verifies skill description quality # # Honest limit: this only verifies strings are present in SKILL.md description. # It cannot prove Claude Code's orchestrator fires the skill on those prompts. # Runtime auto-fire validation is the operator's dogfood step (SC1). # # Checks: # - SKILL.md frontmatter has 'description:' field # - description block (from `description: |` to the closing `---`) is >=400 chars # - if .triggers.txt exists, every phrase in it appears in SKILL.md description # # Usage: bash tests/test-skill-triggers.sh # Exit codes: 0 = pass; 1 = at least one FAIL set -euo pipefail LC_ALL=en_US.UTF-8 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" PASS=0 FAIL=0 WARN=0 pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); } fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); } warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); } echo "=== test-skill-triggers ===" echo "Plugin root: $PLUGIN_ROOT" echo "" # ------------------------------------------------------- # Iterate over every SKILL.md # ------------------------------------------------------- SKILL_COUNT=0 for skill_file in "$PLUGIN_ROOT"/skills/*/SKILL.md; do [ -f "$skill_file" ] || continue SKILL_COUNT=$((SKILL_COUNT + 1)) skill_dir="$(dirname "$skill_file")" skill_name="$(basename "$skill_dir")" echo "--- $skill_name ---" # ------------------------ # Frontmatter check # ------------------------ first_line="$(head -n 1 "$skill_file")" if [ "$first_line" != "---" ]; then fail "$skill_name/SKILL.md: missing frontmatter delimiter on line 1" echo "" continue fi # ------------------------ # Description >=400 chars (Triggers on: enumeration counts) # ------------------------ desc_block_chars="$(awk '/^description: \|/,/^---$/' "$skill_file" | wc -c | tr -d '[:space:]')" if [ -z "$desc_block_chars" ]; then desc_block_chars=0; fi if [ "$desc_block_chars" -ge 400 ]; then pass "$skill_name: description block is $desc_block_chars chars (>=400)" else fail "$skill_name: description block is $desc_block_chars chars (<400 required)" fi # ------------------------ # Trigger-phrase coverage (.triggers.txt) # ------------------------ triggers_file="$skill_dir/.triggers.txt" if [ ! -f "$triggers_file" ]; then warn "$skill_name: .triggers.txt missing (advisory — operator may want one)" echo "" continue fi trigger_count="$(grep -cE '.' "$triggers_file" || true)" if [ -z "$trigger_count" ] || [ "$trigger_count" -lt 8 ]; then fail "$skill_name/.triggers.txt: only $trigger_count phrase(s) (>=8 required)" else pass "$skill_name/.triggers.txt: $trigger_count phrase(s)" fi # check each phrase appears in SKILL.md description block MISSING=0 while IFS= read -r phrase; do [ -z "$phrase" ] && continue if ! grep -qF "$phrase" "$skill_file"; then fail "$skill_name: trigger phrase missing from SKILL.md description: '$phrase'" MISSING=$((MISSING + 1)) fi done < "$triggers_file" if [ "$MISSING" -eq 0 ]; then pass "$skill_name: all $trigger_count trigger phrase(s) appear in SKILL.md" fi echo "" done if [ "$SKILL_COUNT" -eq 0 ]; then fail "no SKILL.md found under skills/*/" fi # ------------------------------------------------------- # Summary # ------------------------------------------------------- echo "=== Summary ===" printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN" if [ "$FAIL" -gt 0 ]; then exit 1 fi exit 0 # Triggers on: documented in each skill's .triggers.txt sibling file.