diff --git a/custom-skills/17-seo-schema-generator/SKILL.md b/custom-skills/17-seo-schema-generator/SKILL.md index 8c8299d..d806b49 100644 --- a/custom-skills/17-seo-schema-generator/SKILL.md +++ b/custom-skills/17-seo-schema-generator/SKILL.md @@ -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, 스키마 생성, 스키마 저작, 구조화 데이터 생성, 미발행 사이트 스키마, 기존 사이트 스키마 추출. diff --git a/custom-skills/92-tui-design-template/SKILL.md b/custom-skills/92-tui-design-template/SKILL.md index 82e43a3..1fb741b 100644 --- a/custom-skills/92-tui-design-template/SKILL.md +++ b/custom-skills/92-tui-design-template/SKILL.md @@ -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 diff --git a/custom-skills/92-tui-design-template/code/SKILL.md b/custom-skills/92-tui-design-template/code/SKILL.md index 57fa407..e6fa91f 100644 --- a/custom-skills/92-tui-design-template/code/SKILL.md +++ b/custom-skills/92-tui-design-template/code/SKILL.md @@ -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 diff --git a/scripts/verify_skills.py b/scripts/verify_skills.py new file mode 100644 index 0000000..a3b3986 --- /dev/null +++ b/scripts/verify_skills.py @@ -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())