spb/ultra-sharp-agent-skills Public
Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.
Python 100%
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai4#5# validate_skills.py — deterministic linter enforcing the Sharp Skill Checklist6# (see RESEARCH-SYNTHESIS.md) on every skill in this repository. Stdlib only.7#8# Usage: python3 tools/validate_skills.py [repo_root]9# Exit codes: 0 = all skills pass, 1 = at least one failure, 2 = usage error.1011import re12import sys13from pathlib import Path1415# Spec limits from Anthropic's Agent Skills documentation.16NAME_MAX = 6417DESC_MAX = 102418BODY_MAX_LINES = 5001920NAME_RE = re.compile(r"^[a-z0-9-]+$")21# Reserved words are forbidden anywhere in the skill name per the spec.22RESERVED = ("anthropic", "claude")2324HEADER_AUTHOR = "Author: Simon-Pierre Boucher"25HEADER_CONTACT = "Contact: contact@spboucher.ai"2627# First/second-person openings that indicate a wrong point of view in a28# description, which the docs warn causes discovery problems.29BAD_POV = re.compile(r"\b(I can|I will|you can use this)\b", re.IGNORECASE)3031MD_LINK = re.compile(r"\]\(([^)#>][^)]*)\)")323334def parse_frontmatter(text):35 """Return (fields dict, body) or (None, text) if no frontmatter."""36 if not text.startswith("---\n"):37 return None, text38 end = text.find("\n---", 4)39 if end == -1:40 return None, text41 fields = {}42 for line in text[4:end].splitlines():43 if ":" in line and not line.startswith((" ", "\t", "#")):44 k, v = line.split(":", 1)45 fields[k.strip()] = v.strip().strip("\"'")46 return fields, text[end + 4:]474849def check_header(path, text):50 """Every project file must carry the author header near the top."""51 head = "\n".join(text.splitlines()[:12])52 return HEADER_AUTHOR in head and HEADER_CONTACT in head535455def lint_skill(skill_md, errors):56 text = skill_md.read_text(encoding="utf-8")57 rel = skill_md58 fm, body = parse_frontmatter(text)5960 if fm is None:61 errors.append(f"{rel}: missing or unterminated YAML frontmatter")62 return6364 name = fm.get("name", "")65 desc = fm.get("description", "")6667 if not name:68 errors.append(f"{rel}: frontmatter missing 'name'")69 else:70 if len(name) > NAME_MAX:71 errors.append(f"{rel}: name exceeds {NAME_MAX} chars")72 if not NAME_RE.match(name):73 errors.append(f"{rel}: name must be lowercase letters/numbers/hyphens")74 if any(w in name for w in RESERVED):75 errors.append(f"{rel}: name contains a reserved word {RESERVED}")76 # CLAUDE.md mandates 'skill-N-<name>' folders for the example skills;77 # the frontmatter name must match the folder minus that prefix.78 folder = re.sub(r"^skill-\d+-", "", skill_md.parent.name)79 if name != folder:80 errors.append(f"{rel}: name '{name}' != folder '{folder}'")8182 if not desc:83 errors.append(f"{rel}: frontmatter missing 'description'")84 else:85 if len(desc) > DESC_MAX:86 errors.append(f"{rel}: description exceeds {DESC_MAX} chars ({len(desc)})")87 if "<" in desc and ">" in desc:88 errors.append(f"{rel}: description may contain XML tags")89 if "Use when" not in desc and "use when" not in desc:90 errors.append(f"{rel}: description lacks a 'Use when …' trigger clause")91 if "Do not use" not in desc and "do not use" not in desc:92 errors.append(f"{rel}: description lacks a 'Do not use for …' boundary")93 if BAD_POV.search(desc):94 errors.append(f"{rel}: description not in third person")9596 if not check_header(skill_md.parent / "SKILL.md", body):97 errors.append(f"{rel}: author header missing after frontmatter")9899 n_lines = len(body.splitlines())100 if n_lines > BODY_MAX_LINES:101 errors.append(f"{rel}: body has {n_lines} lines (max {BODY_MAX_LINES})")102103 for target in MD_LINK.findall(body):104 if target.startswith(("http://", "https://", "mailto:")):105 continue106 if "\\" in target:107 errors.append(f"{rel}: backslash path in link '{target}'")108 continue109 if not (skill_md.parent / target).exists():110 errors.append(f"{rel}: broken reference link '{target}'")111112 for sub in skill_md.parent.rglob("*"):113 if sub.is_file() and sub != skill_md and sub.suffix in (".md", ".py", ".sh"):114 if not check_header(sub, sub.read_text(encoding="utf-8", errors="replace")):115 errors.append(f"{sub}: author header missing")116117118def main():119 root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")120 if not root.is_dir():121 print(f"error: not a directory: {root}", file=sys.stderr)122 sys.exit(2)123124 skill_files = sorted(root.rglob("SKILL.md"))125 if not skill_files:126 print(f"error: no SKILL.md files found under {root}", file=sys.stderr)127 sys.exit(2)128129 errors = []130 for skill_md in skill_files:131 lint_skill(skill_md, errors)132133 print(f"checked {len(skill_files)} skill(s)")134 if errors:135 for e in errors:136 print(f"FAIL {e}")137 print(f"{len(errors)} failure(s)")138 sys.exit(1)139 print("all checks passed")140 sys.exit(0)141142143if __name__ == "__main__":144 main()145