Files
skills/scripts/lint-skills.py

86 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""lint-skills.py — Agent Skills spec lint over a skills catalog.
Checks every SKILL.md:
1. frontmatter parses as YAML
2. `description` present and <= 1024 chars (spec cap)
3. required fields present: name, description (version optional-but-encouraged)
Exit code 0 = clean, 1 = violations found (prints them).
Usage: python lint-skills.py [skills-dir] (default: ./skills)
"""
import os
import sys
try:
import yaml
except ImportError:
print("error: PyYAML required (pip install pyyaml)", file=sys.stderr)
sys.exit(2)
MAX_DESC = 1024
REQUIRED = ("name", "description")
def lint_one(path):
"""Return list of violation strings for one SKILL.md."""
problems = []
txt = open(path, encoding="utf-8").read()
if not txt.startswith("---"):
return [f"{path}: missing frontmatter (must start with ---)"]
try:
fm = txt.split("---", 2)[1]
d = yaml.safe_load(fm)
except Exception as e:
return [f"{path}: frontmatter YAML parse error: {str(e)[:80]}"]
if not isinstance(d, dict):
return [f"{path}: frontmatter is not a mapping"]
for f in REQUIRED:
if not d.get(f):
problems.append(f"{path}: missing required field '{f}'")
desc = d.get("description") or ""
if len(desc) > MAX_DESC:
problems.append(f"{path}: description exceeds {MAX_DESC} chars ({len(desc)})")
return problems
def warn_one(path):
"""Soft findings (not spec violations)."""
warnings = []
txt = open(path, encoding="utf-8").read()
if not txt.startswith("---"):
return warnings
d = yaml.safe_load(txt.split("---", 2)[1])
if isinstance(d, dict) and not d.get("version"):
warnings.append(f"{path}: missing version (encouraged)")
return warnings
def main():
root = sys.argv[1] if len(sys.argv) > 1 else "skills"
if not os.path.isdir(root):
print(f"error: not a directory: {root}", file=sys.stderr)
return 2
all_problems = []
all_warnings = []
count = 0
for dirpath, dirnames, filenames in os.walk(root):
if "SKILL.md" in filenames:
count += 1
p = os.path.join(dirpath, "SKILL.md")
all_problems.extend(lint_one(p))
all_warnings.extend(warn_one(p))
for w in all_warnings:
print(" W: " + w)
if all_problems:
print(f"lint: {count} skills, {len(all_problems)} violations:")
for p in all_problems:
print(" " + p)
return 1
print(f"lint: {count} skills, 0 violations ({len(all_warnings)} warnings)")
return 0
if __name__ == "__main__":
sys.exit(main())