Files
Anthropic-Cybersecurity-Skills/tools/generate-index.py
T
Mahipal 796d96c413 fix: replace three hand-rolled YAML parsers with a shared PyYAML loader
index.json shipped 604 of 817 descriptions truncated to their first line.
The cause was the inline regex parser in update-index.yml: it reconstructed
multi-line descriptions only for the YAML block-scalar styles ('>' and '|')
and silently dropped continuation lines for every other style.

A census of the corpus explains the blast radius:

  block scalar   (description: >-)    43
  single-quoted multiline            278
  plain unquoted multiline           496
  single-line                          0

So 774 of 817 skills (94.7%) used a style the parser mishandled. Commit
d56fc0a7 had fixed only the 43 block-scalar files, and CONTRIBUTING.md
recommends that one working style, which is why it stayed hidden.

- add tools/skill_frontmatter.py, the single PyYAML-backed loader
- add tools/generate-index.py so generation is testable outside CI, with
  a --check mode for use as a gate
- delete the hand-rolled parsers from validate-skill.py (98 lines) and
  validate-agentskills.py, routing both through the shared loader
- implement the reserved-word check that agentskills-skill.schema.json
  names validate-agentskills.py as the enforcement point for

Verified by a differential harness against yaml.safe_load ground truth:
index-vs-source mismatches 606 -> 0.
2026-08-23 17:15:12 +02:00

110 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Generate index.json from the SKILL.md files under skills/.
Previously this logic lived as an inline heredoc inside
.github/workflows/update-index.yml with a hand-rolled regex YAML parser that
truncated 604 of 817 descriptions. It lives here now so it is testable outside
CI and shares one PyYAML-backed parser with every other tool.
Usage:
python tools/generate-index.py # write index.json
python tools/generate-index.py --check # verify index.json is current (CI)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from datetime import datetime, timezone
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from skill_frontmatter import description_of, iter_skill_dirs, load_frontmatter, FrontmatterError
INDEX_VERSION = "1.1.0"
REPOSITORY = "https://github.com/mukul975/Anthropic-Cybersecurity-Skills"
DEFAULT_DOMAIN = "cybersecurity"
def build_index(skills_dir: str = "skills") -> tuple[dict, list[str]]:
"""Build the index payload. Returns (index, errors)."""
skills = []
errors = []
for slug, skill_dir in iter_skill_dirs(skills_dir):
try:
frontmatter = load_frontmatter(os.path.join(skill_dir, "SKILL.md"))
except FrontmatterError as exc:
errors.append(f"{slug}: {exc}")
continue
description = description_of(frontmatter)
if not description:
errors.append(f"{slug}: empty description")
skills.append({
"name": slug,
"description": description,
"domain": frontmatter.get("domain") or DEFAULT_DOMAIN,
"path": f"{skills_dir}/{slug}",
})
index = {
"version": INDEX_VERSION,
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"repository": REPOSITORY,
"domain": DEFAULT_DOMAIN,
"total_skills": len(skills),
"skills": skills,
}
return index, errors
def _comparable(index: dict) -> str:
"""Serialize an index ignoring generated_at, so --check tolerates a re-run."""
return json.dumps({k: v for k, v in index.items() if k != "generated_at"}, sort_keys=True)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true",
help="verify index.json matches the skills tree; do not write")
parser.add_argument("--skills-dir", default="skills")
parser.add_argument("--out", default="index.json")
args = parser.parse_args()
if not os.path.isdir(args.skills_dir):
print(f"ERROR: '{args.skills_dir}' not found. Run from the repository root.")
return 1
index, errors = build_index(args.skills_dir)
for error in errors:
print(f"ERROR {error}")
if errors:
print(f"\n{len(errors)} skill(s) could not be indexed.")
return 1
if args.check:
if not os.path.isfile(args.out):
print(f"ERROR: {args.out} is missing. Run: python tools/generate-index.py")
return 1
with open(args.out, encoding="utf-8") as handle:
current = json.load(handle)
if _comparable(current) != _comparable(index):
print(f"ERROR: {args.out} is out of date. Run: python tools/generate-index.py")
return 1
print(f"OK: {args.out} is up to date ({index['total_skills']} skills)")
return 0
with open(args.out, "w", encoding="utf-8") as handle:
json.dump(index, handle, separators=(",", ":"))
print(f"Updated {args.out}: {index['total_skills']} skills")
return 0
if __name__ == "__main__":
sys.exit(main())