diff --git a/scripts/validate-skills.sh b/scripts/validate-skills.sh index 5030687..1073631 100755 --- a/scripts/validate-skills.sh +++ b/scripts/validate-skills.sh @@ -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 @@ -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 @@ -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 ""