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>
This commit is contained in:
128
scripts/migrate_skill_root.py
Normal file
128
scripts/migrate_skill_root.py
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user