fix(skills): make all skills load-valid + add scripts/verify_skills.py

Add a comprehensive load verifier and fix the two issues it found:

- scripts/verify_skills.py: validates every loadable unit (flat root, suite sub-skill,
  plugin skill) with real YAML parsing, name regex + global uniqueness, frontmatter
  <=1024, description sanity, plugin.json JSON validity, and orphan detection. Read-only.
- 92-tui-design-template (root + code/SKILL.md): fix invalid YAML `triggers:` block
  (`- "a", "b"` multi-scalar list items) -> one phrase per list item.
- 17-seo-schema-generator: remove ">" from description ("generate -> validate" ->
  "generate then validate"); angle brackets are disallowed in descriptions.

Result: 75/75 loadable skills valid — 0 failures, 0 name collisions, 0 orphans,
0 plugin-manifest errors (65 flat + 3 plugins + 7 suite sub-skills).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 01:22:33 +09:00
parent 95d6fdf499
commit 137b927477
4 changed files with 203 additions and 9 deletions

View File

@@ -6,7 +6,7 @@ description: |
(2) from collected sources for a not-yet-published site — reconcile conflicting
facts into a provenance-tracked claims register. Both modes emit the same claims
register, build pruned drafts from type templates (no placeholders shipped), and
hand off to 16-seo-schema-validator (generate -> validate, gate = zero P0).
hand off to 16-seo-schema-validator (generate then validate, gate = zero P0).
Triggers: generate schema, create JSON-LD, schema markup, structured data generator,
source-to-schema, pre-launch schema, claims register, 스키마 생성, 스키마 저작,
구조화 데이터 생성, 미발행 사이트 스키마, 기존 사이트 스키마 추출.

View File

@@ -3,10 +3,18 @@ name: 92-tui-design-template
description: Build Norton Commander / Gopher style TUI wizard interfaces for CLI tools using Python Rich. Covers architecture, components, keyboard input, bilingual i18n, and battle-tested gotchas.
version: 1.0.0
triggers:
- "build TUI", "TUI wizard", "terminal UI", "CLI wizard"
- "Norton Commander style", "Gopher style", "retro TUI"
- "Rich TUI", "interactive CLI", "keyboard navigation"
- "dual panel interface", "terminal wizard"
- build TUI
- TUI wizard
- terminal UI
- CLI wizard
- Norton Commander style
- Gopher style
- retro TUI
- Rich TUI
- interactive CLI
- keyboard navigation
- dual panel interface
- terminal wizard
tools:
- Read
- Write

View File

@@ -3,10 +3,18 @@ name: tui-design-template
description: Build Norton Commander / Gopher style TUI wizard interfaces for CLI tools using Python Rich. Covers architecture, components, keyboard input, bilingual i18n, and battle-tested gotchas.
version: 1.0.0
triggers:
- "build TUI", "TUI wizard", "terminal UI", "CLI wizard"
- "Norton Commander style", "Gopher style", "retro TUI"
- "Rich TUI", "interactive CLI", "keyboard navigation"
- "dual panel interface", "terminal wizard"
- build TUI
- TUI wizard
- terminal UI
- CLI wizard
- Norton Commander style
- Gopher style
- retro TUI
- Rich TUI
- interactive CLI
- keyboard navigation
- dual panel interface
- terminal wizard
tools:
- Read
- Write

178
scripts/verify_skills.py Normal file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
verify_skills.py — verify every skill in custom-skills/ would load correctly.
Checks each loadable unit (flat root skill, suite sub-skill, or plugin skill) against
the repo's canonical rules (skill-creator/quick_validate.py) PLUS:
- frontmatter is valid YAML (parsed, not just regex-matched),
- frontmatter <= 1024 chars (Agent Skills spec),
- name matches ^[a-z0-9-]+$, no leading/trailing/double hyphen,
- description present, no angle brackets,
- skill `name` values are globally unique,
- plugin.json files parse as JSON and carry a name,
- every top-level custom-skills/ entry resolves to >=1 loadable skill (no orphans).
Exit 0 if everything loads; 1 otherwise. Read-only.
"""
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
import yaml
SKILLS = Path(__file__).resolve().parent.parent / "custom-skills"
NAME_RE = re.compile(r"^[a-z0-9-]+$")
FM_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
def validate_skill_md(path):
"""Return (name, errors[]). Mirrors quick_validate.py + YAML + size."""
errors = []
text = path.read_text(encoding="utf-8")
if not text.startswith("---"):
return None, ["no YAML frontmatter"]
m = FM_RE.match(text)
if not m:
return None, ["invalid frontmatter format"]
fm = m.group(1)
if len(fm) > 1024:
errors.append(f"frontmatter {len(fm)}>1024 chars")
try:
data = yaml.safe_load(fm)
except yaml.YAMLError as exc:
return None, [f"YAML parse error: {str(exc).splitlines()[0]}"]
if not isinstance(data, dict):
return None, ["frontmatter is not a mapping"]
name = data.get("name")
if not name:
errors.append("missing 'name'")
else:
name = str(name).strip()
if not NAME_RE.match(name):
errors.append(f"name '{name}' not [a-z0-9-]")
if name.startswith("-") or name.endswith("-") or "--" in name:
errors.append(f"name '{name}' bad hyphen placement")
desc = data.get("description")
if not desc:
errors.append("missing 'description'")
elif "<" in str(desc) or ">" in str(desc):
errors.append("description contains angle brackets (< or >)")
return (name if isinstance(name, str) else None), errors
def sub_skill_skills(d):
"""Sub-skill SKILL.md files (suite members), excluding the dir's own
code/ and desktop/ packaging copies."""
return [p for p in sorted(d.glob("*/SKILL.md"))
if p.parent.name not in ("code", "desktop")]
def classify_top_level(d):
"""How does this top-level dir expose loadable skill(s)?"""
if (d / "SKILL.md").exists():
kind = "flat"
elif (d / ".claude-plugin" / "plugin.json").exists():
kind = "plugin"
elif sub_skill_skills(d):
kind = "suite"
else:
kind = "orphan"
return kind
def main():
units = [] # (label, path) every loadable SKILL.md
top_report = [] # (dir, kind, note)
plugin_errors = []
for d in sorted(SKILLS.iterdir()):
if not d.is_dir() or d.name.startswith("_") or d.name.endswith("-shared"):
continue
kind = classify_top_level(d)
note = ""
if kind == "flat":
units.append((d.name, d / "SKILL.md"))
# a suite container may also have sub-skills
for sub in sub_skill_skills(d):
units.append((f"{d.name}/{sub.parent.name}", sub))
elif kind == "plugin":
pj = d / ".claude-plugin" / "plugin.json"
try:
meta = json.loads(pj.read_text(encoding="utf-8"))
if not meta.get("name"):
plugin_errors.append((d.name, "plugin.json missing name"))
note = f"plugin '{meta.get('name','?')}'"
except json.JSONDecodeError as exc:
plugin_errors.append((d.name, f"plugin.json invalid JSON: {exc}"))
sks = sorted(d.glob("skills/*/SKILL.md"))
if not sks:
note += " — NO skills/*/SKILL.md"
for sk in sks:
units.append((f"{d.name}/skills/{sk.parent.name}", sk))
elif kind == "suite":
for sub in sub_skill_skills(d):
units.append((f"{d.name}/{sub.parent.name}", sub))
top_report.append((d.name, kind, note))
# validate every loadable SKILL.md
results = []
names = defaultdict(list)
for label, path in units:
name, errs = validate_skill_md(path)
results.append((label, name, errs))
if name:
names[name].append(label)
collisions = {n: ls for n, ls in names.items() if len(ls) > 1}
failed = [(l, n, e) for l, n, e in results if e]
orphans = [d for d, k, _ in top_report if k == "orphan"]
# ---- report ----
print(f"Loadable skills checked: {len(results)} "
f"(flat+suite+plugin across {len(top_report)} top-level dirs)\n")
if failed:
print("FAILURES:")
for label, name, errs in failed:
print(f"{label}: {'; '.join(errs)}")
print()
if plugin_errors:
print("PLUGIN MANIFEST ERRORS:")
for d, e in plugin_errors:
print(f"{d}: {e}")
print()
if collisions:
print("NAME COLLISIONS (would shadow each other):")
for n, ls in sorted(collisions.items()):
print(f"'{n}': {', '.join(ls)}")
print()
if orphans:
print("ORPHAN DIRS (no loadable skill):")
for d in orphans:
print(f"{d}")
print()
by_kind = defaultdict(int)
for _, k, _ in top_report:
by_kind[k] += 1
print("Top-level dirs by kind: " +
", ".join(f"{k}={v}" for k, v in sorted(by_kind.items())))
ok = not (failed or plugin_errors or collisions or orphans)
print(f"\n{'✅ ALL SKILLS LOAD CORRECTLY' if ok else '❌ ISSUES FOUND'} "
f"{len(results)-len(failed)}/{len(results)} skills valid, "
f"{len(collisions)} name collisions, {len(orphans)} orphans, "
f"{len(plugin_errors)} plugin errors")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())