Add agentskills.io compliance schema and validator #168
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate SKILL.md files | |
| on: | |
| push: | |
| paths: | |
| - 'skills/**' | |
| pull_request: | |
| paths: | |
| - 'skills/**' | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| name: Validate SKILL.md frontmatter | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Single source of truth: tools/validate-skill.py validates required | |
| # frontmatter fields, kebab-case name, description length, subdomain, and | |
| # tag count. (Previously this step duplicated a weaker inline parser.) | |
| - name: Validate SKILL.md frontmatter | |
| run: python3 tools/validate-skill.py --all | |
| - name: Check for duplicate skill names | |
| run: | | |
| python3 << 'EOF' | |
| import os | |
| import re | |
| from collections import Counter | |
| names = [] | |
| for root, dirs, files in os.walk('skills'): | |
| for file in files: | |
| if file == 'SKILL.md': | |
| path = os.path.join(root, file) | |
| with open(path, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) | |
| if fm_match: | |
| name_match = re.search(r'^name:\s*(.+)$', fm_match.group(1), re.MULTILINE) | |
| if name_match: | |
| names.append(name_match.group(1).strip().strip('"')) | |
| duplicates = [name for name, count in Counter(names).items() if count > 1] | |
| if duplicates: | |
| print(f"❌ Duplicate skill names found: {duplicates}") | |
| exit(1) | |
| print(f"✅ No duplicate names in {len(names)} skills") | |
| EOF | |
| - name: Report skill counts | |
| if: always() | |
| run: | | |
| echo "## Skill Database Stats" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| python3 << 'EOF' | |
| import os | |
| import re | |
| from collections import Counter | |
| subdomain_counts = Counter() | |
| total = 0 | |
| for root, dirs, files in os.walk('skills'): | |
| for file in files: | |
| if file == 'SKILL.md': | |
| total += 1 | |
| path = os.path.join(root, file) | |
| with open(path, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) | |
| if fm_match: | |
| sd_match = re.search(r'^subdomain:\s*(.+)$', fm_match.group(1), re.MULTILINE) | |
| if sd_match: | |
| subdomain_counts[sd_match.group(1).strip()] += 1 | |
| print(f"**Total Skills: {total}**") | |
| print("") | |
| print("| Subdomain | Count |") | |
| print("|-----------|-------|") | |
| for sd, count in sorted(subdomain_counts.items(), key=lambda x: -x[1]): | |
| print(f"| {sd} | {count} |") | |
| EOF |