Files
our-claude-skills/scripts/migrate_skill_root.py
Andrew Yim e519a49cc4 feat(skills): bulk-add root SKILL.md to 61 skills (native Agent Skills loadability)
Run the additive migration pass from SKILL-MIGRATION-GUIDE: generate a root SKILL.md
for every skill that lacked one, copied from its desktop/SKILL.md (or code/SKILL.md),
with name set to the directory name and description + body preserved verbatim.

- scripts/migrate_skill_root.py: the reusable, non-destructive migrator (dry-run default).
- 61 new root SKILL.md (desktop source for most; code/SKILL.md for 61/62/92).
- Untouched: 16/17/95 (already had root); desktop/ and code/ packaging left intact.
- All 64 root SKILL.md validate: frontmatter <=1024, kebab name, description present.

Still MANUAL (no SKILL.md source — commands/README only), need hand-authored root SKILL.md:
81-mac-optimizer, 90-reference-curator, 91-multi-agent-guide, 94-dintel-bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:05:20 +09:00

129 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
migrate_skill_root.py — additive bulk migration: give each skill a root SKILL.md.
Non-destructive. For every skill directory under custom-skills/ that lacks a root
SKILL.md, it generates one by copying the skill's existing directive and:
- setting `name:` to the directory name (guaranteed kebab-case + unique),
- preserving `description` and the markdown body verbatim,
- validating the result (frontmatter <= 1024 chars, kebab name, description present).
Source priority per skill: desktop/SKILL.md -> code/SKILL.md.
It NEVER touches desktop/, code/, or an existing root SKILL.md.
The implementation of the SKILL-MIGRATION-GUIDE recipe, for the whole repo at once.
Usage:
python scripts/migrate_skill_root.py # dry-run (default): report only
python scripts/migrate_skill_root.py --apply # write the new root SKILL.md files
"""
import argparse
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SKILLS = REPO / "custom-skills"
FM_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.S)
MAX_FM = 1024
def is_skill_dir(d):
"""A skill dir is a real skill, not shared infra."""
if not d.is_dir():
return False
n = d.name
return not (n.startswith("_") or n.endswith("-shared"))
def find_source(d):
"""Existing directive to copy from, by priority."""
for cand in (d / "desktop" / "SKILL.md", d / "code" / "SKILL.md"):
if cand.exists():
return cand
return None
def set_name(frontmatter, name):
"""Replace the single-line `name:` value (or prepend one) with `name`."""
if re.search(r"^name:.*$", frontmatter, re.M):
return re.sub(r"^name:.*$", f"name: {name}", frontmatter, count=1, flags=re.M)
return f"name: {name}\n{frontmatter}"
def build_root_skill(src_text, dir_name):
"""Return (new_skill_text, issues[]) or (None, issues) if unusable."""
m = FM_RE.match(src_text)
if not m:
return None, ["source has no YAML frontmatter"]
fm, body = m.group(1), m.group(2).lstrip("\n")
fm = set_name(fm, dir_name)
issues = []
if len(fm) > MAX_FM:
issues.append(f"frontmatter {len(fm)}>{MAX_FM} chars")
if "description:" not in fm:
issues.append("no description")
if not re.search(r"^name:\s*[a-z0-9-]+\s*$", fm, re.M):
issues.append("name not kebab-case")
text = f"---\n{fm}\n---\n\n{body}"
return text, issues
def main(argv=None):
ap = argparse.ArgumentParser(description="Additively add a root SKILL.md to each skill.")
ap.add_argument("--apply", action="store_true", help="write files (default: dry-run)")
args = ap.parse_args(argv)
rows = [] # (dir, status, detail)
created = skipped = manual = warned = 0
for d in sorted(SKILLS.iterdir()):
if not is_skill_dir(d):
continue
name = d.name
if (d / "SKILL.md").exists():
rows.append((name, "SKIP", "already has root SKILL.md"))
skipped += 1
continue
src = find_source(d)
if not src:
rows.append((name, "MANUAL", "no desktop/ or code/ SKILL.md source (commands/README only)"))
manual += 1
continue
text, issues = build_root_skill(src.read_text(encoding="utf-8"), name)
rel = src.relative_to(d)
if text is None:
rows.append((name, "MANUAL", f"{rel}: {issues[0]}"))
manual += 1
continue
if issues:
rows.append((name, "CREATE*", f"from {rel} | FIX: {'; '.join(issues)}"))
warned += 1
else:
rows.append((name, "CREATE", f"from {rel}"))
created += 1
if args.apply:
(d / "SKILL.md").write_text(text, encoding="utf-8")
width = max(len(r[0]) for r in rows)
print(f"{'SKILL':<{width}} STATUS DETAIL")
print(f"{'-'*width} ------- ------")
for name, status, detail in rows:
print(f"{name:<{width}} {status:<7} {detail}")
mode = "APPLIED" if args.apply else "DRY-RUN (no files written)"
print(f"\n[{mode}] create={created} create-with-fix={warned} "
f"skip={skipped} manual={manual} total_skills={len(rows)}")
if warned:
print(" * CREATE* rows were written but need a frontmatter fix (see FIX note).")
if not args.apply:
print(" Re-run with --apply to write the files.")
return 0
if __name__ == "__main__":
sys.exit(main())