Skip to content

Commit ad84dd1

Browse files
authored
Merge pull request #4 from ysskrishna/feature/skills-testing
Add validate-skills.sh and CI workflow for SKILL.md checks
2 parents 48656e0 + fb61229 commit ad84dd1

2 files changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Run validate-skills.sh against skills/*/SKILL.md (name, description, structure).
2+
name: Validate skills
3+
4+
on:
5+
push:
6+
branches: [main]
7+
paths:
8+
- "skills/**"
9+
- "validate-skills.sh"
10+
- ".github/workflows/validate-skills.yml"
11+
pull_request:
12+
branches: [main]
13+
paths:
14+
- "skills/**"
15+
- "validate-skills.sh"
16+
- ".github/workflows/validate-skills.yml"
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
validate:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Validate SKILL.md files
29+
run: bash validate-skills.sh

validate-skills.sh

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/bin/bash
2+
3+
# Colors for output
4+
RED='\033[0;31m'
5+
GREEN='\033[0;32m'
6+
YELLOW='\033[1;33m'
7+
BLUE='\033[0;34m'
8+
NC='\033[0m' # No Color
9+
10+
SKILLS_DIR="skills"
11+
ISSUES=0
12+
WARNINGS=0
13+
PASSED=0
14+
15+
echo "🔍 Auditing Skills Against Agent Skills Specification"
16+
echo "======================================================"
17+
echo ""
18+
echo "Reference: https://agentskills.io/specification.md"
19+
echo ""
20+
21+
# Validation rules from CLAUDE.md
22+
# REQUIRED: name, description
23+
# OPTIONAL: license, metadata
24+
# name: 1-64 chars, lowercase a-z, numbers, hyphens only, must match directory
25+
# description: 1-1024 chars with trigger phrases
26+
# SKILL.md: under 500 lines
27+
# Optional dirs: references/, scripts/, assets/
28+
29+
for skill_dir in "$SKILLS_DIR"/*/; do
30+
skill_name=$(basename "$skill_dir")
31+
skill_file="$skill_dir/SKILL.md"
32+
skill_errors=()
33+
skill_warnings=()
34+
35+
# Check if SKILL.md exists
36+
if [[ ! -f "$skill_file" ]]; then
37+
echo -e "${RED}$skill_name${NC}"
38+
echo " Missing SKILL.md"
39+
((ISSUES++))
40+
continue
41+
fi
42+
43+
# Extract frontmatter (between the first two `---` markers, exclusive)
44+
frontmatter=$(awk '/^---$/{count++; next} count==1' "$skill_file")
45+
46+
# Validate frontmatter exists
47+
if [[ -z "$frontmatter" ]]; then
48+
echo -e "${RED}$skill_name${NC}"
49+
echo " Missing YAML frontmatter (---)"
50+
((ISSUES++))
51+
continue
52+
fi
53+
54+
# ===== NAME VALIDATION =====
55+
name_in_file=$(echo "$frontmatter" | grep "^name:" | sed 's/^name: //' | tr -d ' ')
56+
57+
if [[ -z "$name_in_file" ]]; then
58+
skill_errors+=("Missing 'name' field in frontmatter")
59+
elif [[ "$name_in_file" != "$skill_name" ]]; then
60+
skill_errors+=("Name mismatch: directory='$skill_name' but frontmatter='$name_in_file'")
61+
elif ! [[ "$name_in_file" =~ ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$ ]]; then
62+
skill_errors+=("Invalid name format: '$name_in_file' (must be lowercase, alphanumeric + hyphens only)")
63+
elif [[ ${#name_in_file} -lt 1 || ${#name_in_file} -gt 64 ]]; then
64+
skill_errors+=("Name length invalid: ${#name_in_file} chars (must be 1-64)")
65+
fi
66+
67+
# ===== DESCRIPTION VALIDATION =====
68+
# Extract full description: same-line values, quoted strings, and YAML
69+
# block/folded scalars (description: > or | with indented continuation lines).
70+
description=$(echo "$frontmatter" | awk '
71+
/^description:/ {
72+
sub(/^description:[[:space:]]*/, "")
73+
line = $0
74+
# Double-quoted single-line
75+
if (line ~ /^"/) {
76+
gsub(/^"|"$/, "", line)
77+
print line
78+
exit
79+
}
80+
# Folded (>) or literal (|) block — indicator may include - (strip chomping)
81+
if (line ~ /^(>|>-|[|]|[|]-)[[:space:]]*$/) {
82+
desc = ""
83+
while (getline > 0) {
84+
# Next top-level frontmatter key (unindented name:)
85+
if ($0 ~ /^[a-zA-Z][a-zA-Z0-9_-]*:[[:space:]]/) break
86+
s = $0
87+
sub(/^[[:space:]]+/, "", s)
88+
desc = (desc == "" ? s : desc " " s)
89+
}
90+
print desc
91+
exit
92+
}
93+
print line
94+
exit
95+
}
96+
')
97+
98+
if [[ -z "$description" ]]; then
99+
skill_errors+=("Missing 'description' field in frontmatter")
100+
else
101+
desc_len=${#description}
102+
if [[ $desc_len -lt 1 || $desc_len -gt 1024 ]]; then
103+
skill_errors+=("Description length invalid: $desc_len chars (must be 1-1024)")
104+
fi
105+
106+
# Check for trigger phrases (case-insensitive; match "Use", "when", etc.)
107+
if ! echo "$description" | grep -Eiq 'when|mention|use'; then
108+
skill_warnings+=("Description lacks clear trigger phrases ('when', 'mention', 'use')")
109+
fi
110+
fi
111+
112+
# ===== OPTIONAL FIELDS VALIDATION =====
113+
license=$(echo "$frontmatter" | grep "^license:" | sed 's/^license: //' | tr -d ' ')
114+
if [[ -n "$license" && "$license" != "MIT" && "$license" != "Apache-2.0" && "$license" != "ISC" ]]; then
115+
skill_warnings+=("License '$license' is non-standard (default: MIT)")
116+
fi
117+
118+
# Check metadata structure
119+
metadata=$(echo "$frontmatter" | grep -A 10 "^metadata:")
120+
if [[ -n "$metadata" ]]; then
121+
# If metadata exists, check for version placement
122+
if echo "$frontmatter" | grep -q "^version:"; then
123+
skill_errors+=("'version' is top-level (should be under 'metadata:')")
124+
fi
125+
# Could add more metadata validation here
126+
fi
127+
128+
# ===== FILE STRUCTURE VALIDATION =====
129+
line_count=$(wc -l < "$skill_file")
130+
if [[ $line_count -gt 500 ]]; then
131+
skill_warnings+=("SKILL.md is $line_count lines (should be <500, move details to references/)")
132+
fi
133+
134+
# Check for optional directories
135+
for optdir in references scripts assets; do
136+
if [[ -d "$skill_dir/$optdir" ]]; then
137+
# Just note its presence - no validation required
138+
:
139+
fi
140+
done
141+
142+
# ===== REPORT RESULTS =====
143+
if [[ ${#skill_errors[@]} -gt 0 ]]; then
144+
echo -e "${RED}$skill_name${NC}"
145+
for error in "${skill_errors[@]}"; do
146+
echo -e " ${RED}Error:${NC} $error"
147+
done
148+
if [[ ${#skill_warnings[@]} -gt 0 ]]; then
149+
for warning in "${skill_warnings[@]}"; do
150+
echo -e " ${YELLOW}Warning:${NC} $warning"
151+
done
152+
fi
153+
((ISSUES++))
154+
elif [[ ${#skill_warnings[@]} -gt 0 ]]; then
155+
echo -e "${YELLOW}⚠️ $skill_name${NC}"
156+
for warning in "${skill_warnings[@]}"; do
157+
echo -e " ${YELLOW}Warning:${NC} $warning"
158+
done
159+
((WARNINGS++))
160+
else
161+
echo -e "${GREEN}$skill_name${NC}"
162+
((PASSED++))
163+
fi
164+
done
165+
166+
echo ""
167+
echo "======================================================"
168+
echo "Summary:"
169+
echo -e " ${GREEN}✓ Passed: $PASSED${NC}"
170+
if [[ $WARNINGS -gt 0 ]]; then
171+
echo -e " ${YELLOW}⚠️ Warnings: $WARNINGS${NC}"
172+
fi
173+
if [[ $ISSUES -gt 0 ]]; then
174+
echo -e " ${RED}❌ Issues: $ISSUES${NC}"
175+
fi
176+
echo ""
177+
178+
if [[ $ISSUES -eq 0 ]]; then
179+
echo -e "${GREEN}All skills are valid! ✓${NC}"
180+
exit 0
181+
else
182+
echo -e "${RED}Found $ISSUES issue(s) that need fixing.${NC}"
183+
exit 1
184+
fi

0 commit comments

Comments
 (0)