mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-08-28 12:19:41 +03:00
The description is the only signal an agent sees at discovery time, so overlapping descriptions cause misrouting. Nothing in CI checked for that. - tools/lint-descriptions.py enforces name==folder, description <=1024 chars, terminal punctuation (a truncation canary), a trigger clause, a negative trigger, and a 500-line body cap. Pre-existing failures are grandfathered in tools/lint-baseline.json so this blocks new debt only; the baseline may shrink and never grow. - tools/detect-collisions.py scores every description pair by TF-IDF cosine and ratchets the count of unreviewed near-duplicates. It strips negative-trigger clauses before vectorizing: those name the sibling skill on purpose, so scoring them would make correct disambiguation raise a pair's similarity. - wire both into validate-skills.yml, along with agentskills conformance, an index.json freshness check, and a guard that fails the build if a regex frontmatter parser is reintroduced. - broaden the path filters from tools/validate-skill.py to tools/**, as noted when #105 merged. All five gates verified to fail on deliberately broken input.
123 lines
4.7 KiB
YAML
123 lines
4.7 KiB
YAML
name: Validate SKILL.md files
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- 'skills/**'
|
|
- 'tools/**'
|
|
- '.github/workflows/validate-skills.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'skills/**'
|
|
- 'tools/**'
|
|
- '.github/workflows/validate-skills.yml'
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
name: Validate SKILL.md frontmatter
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install dependencies
|
|
run: pip install pyyaml
|
|
|
|
# All frontmatter is parsed by tools/skill_frontmatter.py (PyYAML). Any
|
|
# reintroduced regex parser silently truncates multi-line descriptions --
|
|
# that bug shipped 604/817 broken descriptions before it was caught.
|
|
- name: Guard against hand-rolled YAML parsers
|
|
run: |
|
|
if grep -rnE '(re\.(search|match|compile)\([^)]*description|^\s*description:.*\(\.\*\))' \
|
|
tools/ --include='*.py' ; then
|
|
echo "::error::Regex-based frontmatter parsing detected. Use tools/skill_frontmatter.py."
|
|
exit 1
|
|
fi
|
|
echo "OK: no regex frontmatter parsers"
|
|
|
|
# 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
|
|
|
|
# agentskills.io conformance: name==directory, 1..1024 description,
|
|
# reserved-word ban, angle-bracket injection check.
|
|
- name: Validate agentskills.io conformance
|
|
run: python3 tools/validate-agentskills.py --strict
|
|
|
|
# index.json is generated; a PR that changes a description must regenerate it.
|
|
- name: Check index.json is current
|
|
run: python3 tools/generate-index.py --check
|
|
|
|
# Description quality gate. Pre-existing failures are grandfathered in
|
|
# tools/lint-baseline.json so this blocks NEW debt only; the baseline is
|
|
# allowed to shrink and never to grow.
|
|
- name: Lint descriptions
|
|
run: python3 tools/lint-descriptions.py --all --stats
|
|
|
|
# Ratchet: the number of unreviewed near-duplicate description pairs may
|
|
# never increase. Lower this cap as disambiguation lands.
|
|
- name: Detect skill collisions
|
|
run: python3 tools/detect-collisions.py --max-unreviewed 55
|
|
|
|
- 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
|