Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions scripts/validate-skills.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
# 1. SKILL.md exists in each skill dir
# 2. YAML frontmatter is present (--- ... ---)
# 3. `name:` field matches the directory name
# 4. `name:` matches ^[a-z0-9-]{1,64}$ and is not "anthropic" or "claude"
# 4. `name:` matches ^[a-z0-9-]{1,64}$ and is not a reserved word
# (anthropic, claude, meta, google)
# 5. `description:` is present, non-empty, and <= 1024 chars
# 6. `description:` contains "Use when" (or "use when") to signal trigger conditions
# 7. SKILL.md body (after frontmatter) is <= 500 lines
# 8. `description:` is in third person — no first/second-person pronouns or "can help"
# (quoted user trigger phrases are exempt — they're verbatim quotes, not narration)
# 9. `description:` contains no XML-style angle brackets (breaks manifest parsing)
# 10. `description:` should not open with a vague verb (Helps, Manages, Tools for, …) — WARN only
# 11. Body (outside fenced code blocks) uses forward slashes, not Windows-style `\`

set -e

Expand Down Expand Up @@ -78,9 +84,11 @@ for skill_dir in "$SKILLS_DIR"/*/; do
if ! echo "$name_value" | grep -Eq '^[a-z0-9-]{1,64}$'; then
fail "name '$name_value' must match ^[a-z0-9-]{1,64}$"
fi
if [ "$name_value" = "anthropic" ] || [ "$name_value" = "claude" ]; then
fail "name '$name_value' is a reserved word"
fi
case "$name_value" in
anthropic|claude|meta|google)
fail "name '$name_value' is a reserved word"
;;
esac
fi

# 5. description present, non-empty, <= 1024 chars
Expand Down Expand Up @@ -116,6 +124,47 @@ for skill_dir in "$SKILLS_DIR"/*/; do
else
ok "body is $body_lines lines"
fi

# ---------------------------------------------------------------------------
# Conservative best-practice checks (added in v0.3).
# Each check passes on the current 7 in-repo skills; tighten in v0.4.
# ---------------------------------------------------------------------------

if [ -n "$description_value" ]; then
# 8. Description in third person.
# Strip "quoted phrases" first — those are verbatim user triggers, not narration.
# Case-sensitive on pronouns so "US" (country) doesn't match "us".
desc_no_quotes="$(echo "$description_value" | sed 's/"[^"]*"//g')"
if echo "$desc_no_quotes" | grep -qE '\b(I|you|we|us|my|your|our)\b' \
|| echo "$desc_no_quotes" | grep -qi 'can help'; then
fail "description uses first/second person — rewrite in third person (e.g., 'Analyzes', 'Designs')"
else
ok "description is in third person"
fi

# 9. No XML-style angle brackets in description (breaks manifest parsing).
if echo "$description_value" | grep -qE '<[a-zA-Z/]'; then
fail "description contains XML-style tags — these break manifest parsing"
else
ok "description has no XML-style tags"
fi

# 10. (WARN, doesn't increment errors.) Description opener should be specific.
case "$description_value" in
"Helps "*|"Manages "*|"Tools for "*|"Utility for "*|"Provides "*)
echo " ⚠ description starts with vague verb — consider a more specific opener (e.g., 'Analyzes', 'Generates', 'Validates')" >&2
;;
esac
fi

# 11. No Windows-style path separators in body (outside fenced code blocks).
# Matches a single literal backslash — Windows paths use `dir\file`.
body_no_code="$(awk -v end="$fm_end" 'NR > end { if ($0 ~ /^```/) { incode = !incode; next } if (!incode) print }' "$skill_md")"
if echo "$body_no_code" | grep -q '\\'; then
fail "body contains Windows-style path separators (\\) outside code blocks — use forward slashes"
else
ok "body uses forward-slash paths"
fi
done

echo ""
Expand Down
Loading