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 (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 facts into a provenance-tracked claims register. Both modes emit the same claims
register, build pruned drafts from type templates (no placeholders shipped), and 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, Triggers: generate schema, create JSON-LD, schema markup, structured data generator,
source-to-schema, pre-launch schema, claims register, 스키마 생성, 스키마 저작, 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. 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 version: 1.0.0
triggers: triggers:
- "build TUI", "TUI wizard", "terminal UI", "CLI wizard" - build TUI
- "Norton Commander style", "Gopher style", "retro TUI" - TUI wizard
- "Rich TUI", "interactive CLI", "keyboard navigation" - terminal UI
- "dual panel interface", "terminal wizard" - CLI wizard
- Norton Commander style
- Gopher style
- retro TUI
- Rich TUI
- interactive CLI
- keyboard navigation
- dual panel interface
- terminal wizard
tools: tools:
- Read - Read
- Write - 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. 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 version: 1.0.0
triggers: triggers:
- "build TUI", "TUI wizard", "terminal UI", "CLI wizard" - build TUI
- "Norton Commander style", "Gopher style", "retro TUI" - TUI wizard
- "Rich TUI", "interactive CLI", "keyboard navigation" - terminal UI
- "dual panel interface", "terminal wizard" - CLI wizard
- Norton Commander style
- Gopher style
- retro TUI
- Rich TUI
- interactive CLI
- keyboard navigation
- dual panel interface
- terminal wizard
tools: tools:
- Read - Read
- Write - 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())