validate-triggers.sh 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/bin/bash
  2. # Validate skill trigger keywords
  3. # Ensures descriptions contain advertised triggers and follows naming conventions
  4. set -euo pipefail
  5. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  6. SKILLS_DIR="$SCRIPT_DIR/../../skills"
  7. # Colors
  8. RED='\033[0;31m'
  9. GREEN='\033[0;32m'
  10. YELLOW='\033[1;33m'
  11. BLUE='\033[0;34m'
  12. NC='\033[0m'
  13. PASSED=0
  14. FAILED=0
  15. WARNINGS=0
  16. pass() { ((PASSED++)); echo -e "${GREEN}✓${NC} $1"; }
  17. fail() { ((FAILED++)); echo -e "${RED}✗${NC} $1: $2"; }
  18. warn() { ((WARNINGS++)); echo -e "${YELLOW}!${NC} $1: $2"; }
  19. # Extract frontmatter field from SKILL.md
  20. get_frontmatter() {
  21. local file="$1"
  22. local field="$2"
  23. # Extract value between --- markers (|| true to prevent set -e failure)
  24. sed -n '/^---$/,/^---$/p' "$file" | grep "^${field}:" | sed "s/^${field}: *//" | sed 's/^"//' | sed 's/"$//' || true
  25. }
  26. # Validate skill name format
  27. validate_name() {
  28. local skill_dir="$1"
  29. local name="$2"
  30. local dirname
  31. dirname=$(basename "$skill_dir")
  32. # Check name matches directory
  33. if [[ "$name" != "$dirname" ]]; then
  34. fail "$dirname" "name '$name' doesn't match directory"
  35. return 1
  36. fi
  37. # Check format: lowercase, numbers, hyphens only
  38. if [[ ! "$name" =~ ^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$ ]]; then
  39. fail "$dirname" "name must be lowercase alphanumeric with hyphens"
  40. return 1
  41. fi
  42. # Check length
  43. if [[ ${#name} -gt 64 ]]; then
  44. fail "$dirname" "name exceeds 64 characters"
  45. return 1
  46. fi
  47. return 0
  48. }
  49. # Validate description has triggers
  50. validate_description() {
  51. local skill_dir="$1"
  52. local description="$2"
  53. local dirname
  54. dirname=$(basename "$skill_dir")
  55. # Check non-empty
  56. if [[ -z "$description" ]]; then
  57. fail "$dirname" "description is empty"
  58. return 1
  59. fi
  60. # Check length
  61. if [[ ${#description} -gt 1024 ]]; then
  62. fail "$dirname" "description exceeds 1024 characters"
  63. return 1
  64. fi
  65. # Check for trigger keywords
  66. if [[ "$description" != *"Triggers on"* && "$description" != *"triggers on"* && "$description" != *"Auto-activates"* ]]; then
  67. warn "$dirname" "no 'Triggers on:' section in description"
  68. fi
  69. return 0
  70. }
  71. # Extract and validate trigger keywords
  72. validate_triggers() {
  73. local skill_dir="$1"
  74. local description="$2"
  75. local dirname
  76. dirname=$(basename "$skill_dir")
  77. # Extract triggers after "Triggers on" using sed (macOS compatible)
  78. local triggers=""
  79. if [[ "$description" == *"Triggers on:"* ]]; then
  80. triggers=$(echo "$description" | sed -n 's/.*Triggers on:[[:space:]]*//p')
  81. elif [[ "$description" == *"Triggers on "* ]]; then
  82. # Handle "Triggers on X, Y, Z" without colon
  83. triggers=$(echo "$description" | sed -n 's/.*Triggers on[[:space:]]*//p')
  84. elif [[ "$description" == *"triggers on:"* ]]; then
  85. triggers=$(echo "$description" | sed -n 's/.*triggers on:[[:space:]]*//p')
  86. elif [[ "$description" == *"Auto-activates"* ]]; then
  87. triggers=$(echo "$description" | sed -n 's/.*Auto-activates[[:space:]]*//p')
  88. fi
  89. if [[ -n "$triggers" ]]; then
  90. # Count trigger keywords (comma separated)
  91. local count
  92. count=$(echo "$triggers" | tr ',' '\n' | wc -l | tr -d ' ')
  93. if [[ $count -lt 3 ]]; then
  94. warn "$dirname" "only $count trigger keywords (recommend 5+)"
  95. else
  96. pass "$dirname: $count trigger keywords"
  97. fi
  98. fi
  99. }
  100. # Validate required CLI tools are documented
  101. validate_compatibility() {
  102. local skill_dir="$1"
  103. local skill_file="$skill_dir/SKILL.md"
  104. local dirname
  105. dirname=$(basename "$skill_dir")
  106. local compat
  107. compat=$(get_frontmatter "$skill_file" "compatibility")
  108. local content
  109. content=$(cat "$skill_file")
  110. # Check if skill references CLI tools
  111. local needs_tools=false
  112. if [[ "$content" == *"brew install"* || "$content" == *"npm install"* ]]; then
  113. needs_tools=true
  114. fi
  115. if [[ "$needs_tools" == true && -z "$compat" ]]; then
  116. warn "$dirname" "references CLI tools but no compatibility field"
  117. fi
  118. }
  119. # Validate allowed-tools field
  120. validate_allowed_tools() {
  121. local skill_dir="$1"
  122. local skill_file="$skill_dir/SKILL.md"
  123. local dirname
  124. dirname=$(basename "$skill_dir")
  125. local tools
  126. tools=$(get_frontmatter "$skill_file" "allowed-tools")
  127. if [[ -z "$tools" ]]; then
  128. warn "$dirname" "no allowed-tools field"
  129. fi
  130. }
  131. # Main validation
  132. validate_skill() {
  133. local skill_dir="$1"
  134. local skill_file="$skill_dir/SKILL.md"
  135. if [[ ! -f "$skill_file" ]]; then
  136. fail "$(basename "$skill_dir")" "SKILL.md not found"
  137. return
  138. fi
  139. local name description
  140. name=$(get_frontmatter "$skill_file" "name")
  141. description=$(get_frontmatter "$skill_file" "description")
  142. validate_name "$skill_dir" "$name" || true
  143. validate_description "$skill_dir" "$description" || true
  144. validate_triggers "$skill_dir" "$description"
  145. validate_compatibility "$skill_dir"
  146. validate_allowed_tools "$skill_dir"
  147. }
  148. # === Main ===
  149. main() {
  150. echo "=== Skill Trigger Validation ==="
  151. echo ""
  152. local skill_count=0
  153. for skill_dir in "$SKILLS_DIR"/*/; do
  154. if [[ -d "$skill_dir" ]]; then
  155. ((skill_count++))
  156. echo -e "${BLUE}--- $(basename "$skill_dir") ---${NC}"
  157. validate_skill "$skill_dir"
  158. echo ""
  159. fi
  160. done
  161. echo "=== Summary ==="
  162. echo "Skills validated: $skill_count"
  163. echo -e "Passed: ${GREEN}$PASSED${NC}"
  164. echo -e "Failed: ${RED}$FAILED${NC}"
  165. echo -e "Warnings: ${YELLOW}$WARNINGS${NC}"
  166. [[ $FAILED -eq 0 ]]
  167. }
  168. main "$@"