diff --git a/validate-skills.sh b/validate-skills.sh index 6fb7d2a..b6a5535 100755 --- a/validate-skills.sh +++ b/validate-skills.sh @@ -4,6 +4,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +BLUE='\033[0;34m' NC='\033[0m' # No Color SKILLS_DIR="skills" @@ -14,22 +15,27 @@ PASSED=0 echo "🔍 Auditing Skills Against Agent Skills Specification" echo "======================================================" echo "" +echo "Reference: https://agentskills.io/specification.md" +echo "" # Validation rules from CLAUDE.md -# name: 1-64 chars, lowercase a-z, numbers, hyphens only +# REQUIRED: name, description +# OPTIONAL: license, metadata +# name: 1-64 chars, lowercase a-z, numbers, hyphens only, must match directory # description: 1-1024 chars with trigger phrases # SKILL.md: under 500 lines -# Required fields: name, description +# Optional dirs: references/, scripts/, assets/ for skill_dir in "$SKILLS_DIR"/*/; do skill_name=$(basename "$skill_dir") skill_file="$skill_dir/SKILL.md" - - echo -n "📋 $skill_name: " + skill_errors=() + skill_warnings=() # Check if SKILL.md exists if [[ ! -f "$skill_file" ]]; then - echo -e "${RED}❌ Missing SKILL.md${NC}" + echo -e "${RED}❌ $skill_name${NC}" + echo " Missing SKILL.md" ((ISSUES++)) continue fi @@ -37,69 +43,109 @@ for skill_dir in "$SKILLS_DIR"/*/; do # Extract frontmatter frontmatter=$(sed -n '/^---$/,/^---$/p' "$skill_file" | head -n -1 | tail -n +2) - # Check name field + # Validate frontmatter exists + if [[ -z "$frontmatter" ]]; then + echo -e "${RED}❌ $skill_name${NC}" + echo " Missing YAML frontmatter (---)" + ((ISSUES++)) + continue + fi + + # ===== NAME VALIDATION ===== name_in_file=$(echo "$frontmatter" | grep "^name:" | sed 's/^name: //' | tr -d ' ') if [[ -z "$name_in_file" ]]; then - echo -e "${RED}❌ Missing name in frontmatter${NC}" - ((ISSUES++)) - continue + skill_errors+=("Missing 'name' field in frontmatter") + elif [[ "$name_in_file" != "$skill_name" ]]; then + skill_errors+=("Name mismatch: directory='$skill_name' but frontmatter='$name_in_file'") + elif ! [[ "$name_in_file" =~ ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$ ]]; then + skill_errors+=("Invalid name format: '$name_in_file' (must be lowercase, alphanumeric + hyphens only)") + elif [[ ${#name_in_file} -lt 1 || ${#name_in_file} -gt 64 ]]; then + skill_errors+=("Name length invalid: ${#name_in_file} chars (must be 1-64)") fi - # Check name matches directory - if [[ "$name_in_file" != "$skill_name" ]]; then - echo -e "${RED}❌ Name mismatch: dir='$skill_name' but frontmatter='$name_in_file'${NC}" - ((ISSUES++)) - continue + # ===== DESCRIPTION VALIDATION ===== + # Handle both quoted and unquoted descriptions + description=$(echo "$frontmatter" | grep "^description:" | head -1) + if [[ $description == *'description: "'* ]]; then + # Quoted description - extract between quotes + description=$(echo "$description" | sed 's/^description: "//' | sed 's/"$//') + else + # Unquoted description + description=$(echo "$description" | sed 's/^description: //') fi - # Validate name format: lowercase, alphanumeric, hyphens only - if ! [[ "$name_in_file" =~ ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$ ]]; then - echo -e "${RED}❌ Invalid name format: '$name_in_file'${NC}" - ((ISSUES++)) - continue - fi - - # Check name length (1-64 chars) - if [[ ${#name_in_file} -lt 1 || ${#name_in_file} -gt 64 ]]; then - echo -e "${RED}❌ Name length invalid: ${#name_in_file} chars (must be 1-64)${NC}" - ((ISSUES++)) - continue - fi - - # Extract description - description=$(echo "$frontmatter" | grep "^description:" | sed 's/^description: //' | sed 's/^"//' | sed 's/"$//') - if [[ -z "$description" ]]; then - echo -e "${RED}❌ Missing description in frontmatter${NC}" - ((ISSUES++)) - continue + skill_errors+=("Missing 'description' field in frontmatter") + else + desc_len=${#description} + if [[ $desc_len -lt 1 || $desc_len -gt 1024 ]]; then + skill_errors+=("Description length invalid: $desc_len chars (must be 1-1024)") + fi + + # Check for trigger phrases (When, when to use, mentions, etc.) + if ! echo "$description" | grep -qi "when\|mention\|use"; then + skill_warnings+=("Description lacks clear trigger phrases ('when', 'mention', 'use')") + fi + + # Check for related skills reference (scope boundaries) + if ! echo "$description" | grep -qi "see\|for\|ref"; then + skill_warnings+=("Description lacks related skills reference (e.g., 'For X, see Y')") + fi fi - # Check description length (1-1024 chars) - desc_len=${#description} - if [[ $desc_len -lt 1 || $desc_len -gt 1024 ]]; then - echo -e "${RED}❌ Description length invalid: $desc_len chars (must be 1-1024)${NC}" - ((ISSUES++)) - continue + # ===== OPTIONAL FIELDS VALIDATION ===== + license=$(echo "$frontmatter" | grep "^license:" | sed 's/^license: //' | tr -d ' ') + if [[ -n "$license" && "$license" != "MIT" && "$license" != "Apache-2.0" && "$license" != "ISC" ]]; then + skill_warnings+=("License '$license' is non-standard (default: MIT)") fi - # Check for trigger phrases in description (at least "also use when" or similar) - if ! echo "$description" | grep -qi "when\|also\|mention\|use"; then - echo -e "${YELLOW}⚠️ Warning: Description lacks trigger phrases${NC}" - ((WARNINGS++)) + # Check metadata structure + metadata=$(echo "$frontmatter" | grep -A 10 "^metadata:") + if [[ -n "$metadata" ]]; then + # If metadata exists, check for version placement + if echo "$frontmatter" | grep -q "^version:"; then + skill_errors+=("'version' is top-level (should be under 'metadata:')") + fi + # Could add more metadata validation here fi - # Count lines in SKILL.md + # ===== FILE STRUCTURE VALIDATION ===== line_count=$(wc -l < "$skill_file") if [[ $line_count -gt 500 ]]; then - echo -e "${YELLOW}⚠️ Warning: SKILL.md is $line_count lines (should be <500)${NC}" - ((WARNINGS++)) + skill_warnings+=("SKILL.md is $line_count lines (should be <500, move details to references/)") fi - # All checks passed - echo -e "${GREEN}✓ Valid${NC}" - ((PASSED++)) + # Check for optional directories + for optdir in references scripts assets; do + if [[ -d "$skill_dir/$optdir" ]]; then + # Just note its presence - no validation required + : + fi + done + + # ===== REPORT RESULTS ===== + if [[ ${#skill_errors[@]} -gt 0 ]]; then + echo -e "${RED}❌ $skill_name${NC}" + for error in "${skill_errors[@]}"; do + echo -e " ${RED}Error:${NC} $error" + done + if [[ ${#skill_warnings[@]} -gt 0 ]]; then + for warning in "${skill_warnings[@]}"; do + echo -e " ${YELLOW}Warning:${NC} $warning" + done + fi + ((ISSUES++)) + elif [[ ${#skill_warnings[@]} -gt 0 ]]; then + echo -e "${YELLOW}⚠️ $skill_name${NC}" + for warning in "${skill_warnings[@]}"; do + echo -e " ${YELLOW}Warning:${NC} $warning" + done + ((WARNINGS++)) + else + echo -e "${GREEN}✓ $skill_name${NC}" + ((PASSED++)) + fi done echo ""