#!/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())