@runecraft/grimoire 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/catalog.json +9 -0
  4. package/dist/grimoire.js +1758 -0
  5. package/package.json +54 -0
  6. package/references/definition-of-done.md +67 -0
  7. package/references/testing-patterns.md +260 -0
  8. package/skills/code-review-and-quality/README.md +13 -0
  9. package/skills/code-review-and-quality/SKILL.md +389 -0
  10. package/skills/code-simplification/README.md +13 -0
  11. package/skills/code-simplification/SKILL.md +338 -0
  12. package/skills/debugging-and-error-recovery/README.md +13 -0
  13. package/skills/debugging-and-error-recovery/SKILL.md +343 -0
  14. package/skills/debugging-and-error-recovery/scripts/__pycache__/triage_state.cpython-314.pyc +0 -0
  15. package/skills/debugging-and-error-recovery/scripts/triage_state.py +206 -0
  16. package/skills/deprecation-and-migration/README.md +13 -0
  17. package/skills/deprecation-and-migration/SKILL.md +248 -0
  18. package/skills/deprecation-and-migration/scripts/__pycache__/migration_tracker.cpython-314.pyc +0 -0
  19. package/skills/deprecation-and-migration/scripts/migration_tracker.py +237 -0
  20. package/skills/doubt-driven-development/README.md +13 -0
  21. package/skills/doubt-driven-development/SKILL.md +251 -0
  22. package/skills/git-commit-learning/.skill-meta.json +14 -0
  23. package/skills/git-commit-learning/README.md +205 -0
  24. package/skills/git-commit-learning/SKILL.md +435 -0
  25. package/skills/git-commit-learning/references/commit-patterns.md +595 -0
  26. package/skills/git-worktree/README.md +13 -0
  27. package/skills/git-worktree/SKILL.md +220 -0
  28. package/skills/idea-refine/README.md +13 -0
  29. package/skills/idea-refine/SKILL.md +186 -0
  30. package/skills/interview-me/README.md +13 -0
  31. package/skills/interview-me/SKILL.md +233 -0
  32. package/skills/linkedin-audit/SKILL.md +98 -0
  33. package/skills/linkedin-audit/references/dashboard-spec.md +43 -0
  34. package/skills/memory-management/README.md +13 -0
  35. package/skills/memory-management/SKILL.md +198 -0
  36. package/skills/security-and-hardening/README.md +13 -0
  37. package/skills/security-and-hardening/SKILL.md +472 -0
  38. package/skills/shipping-and-launch/README.md +13 -0
  39. package/skills/shipping-and-launch/SKILL.md +317 -0
  40. package/skills/skill-forge/README.md +153 -0
  41. package/skills/skill-forge/SKILL.md +291 -0
  42. package/skills/skill-forge/assets/SKILL.template.md +73 -0
  43. package/skills/skill-forge/references/authoring-patterns.md +249 -0
  44. package/skills/skill-forge/references/description-optimization.md +171 -0
  45. package/skills/skill-forge/references/output-evaluation.md +276 -0
  46. package/skills/skill-forge/references/scripts-guide.md +232 -0
  47. package/skills/skill-forge/references/spec.md +175 -0
  48. package/skills/skill-forge/scripts/validate.py +536 -0
  49. package/skills/spec-driven/.skill-meta.json +14 -0
  50. package/skills/spec-driven/README.md +335 -0
  51. package/skills/spec-driven/SKILL.md +174 -0
  52. package/skills/spec-driven/references/code-analysis.md +98 -0
  53. package/skills/spec-driven/references/coding-principles.md +56 -0
  54. package/skills/spec-driven/references/context-limits.md +31 -0
  55. package/skills/spec-driven/references/design.md +199 -0
  56. package/skills/spec-driven/references/discuss.md +136 -0
  57. package/skills/spec-driven/references/implement.md +425 -0
  58. package/skills/spec-driven/references/lessons.md +113 -0
  59. package/skills/spec-driven/references/memory.md +126 -0
  60. package/skills/spec-driven/references/specify.md +210 -0
  61. package/skills/spec-driven/references/sub-agents.md +96 -0
  62. package/skills/spec-driven/references/tasks.md +484 -0
  63. package/skills/spec-driven/references/validate.md +350 -0
  64. package/skills/spec-driven/scripts/__pycache__/lessons.cpython-314.pyc +0 -0
  65. package/skills/spec-driven/scripts/lessons.py +370 -0
  66. package/skills/spec-loop/README.md +36 -0
  67. package/skills/spec-loop/SKILL.md +61 -0
  68. package/skills/test-driven-development/README.md +13 -0
  69. package/skills/test-driven-development/SKILL.md +388 -0
  70. package/skills/typescript-patterns/README.md +13 -0
  71. package/skills/typescript-patterns/SKILL.md +346 -0
  72. package/skills/using-agent-skills/README.md +13 -0
  73. package/skills/using-agent-skills/SKILL.md +187 -0
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """
6
+ Validate a skill folder against the open SKILL.md format.
7
+
8
+ Usage:
9
+ python3 scripts/validate.py <path-to-skill-folder>
10
+ python3 scripts/validate.py <path-to-skill-folder> --format json
11
+ python3 scripts/validate.py <path-to-skill-folder> --json-out /tmp/skill-report.json
12
+ python3 scripts/validate.py --help
13
+
14
+ Exit codes:
15
+ 0 = pass (warnings allowed)
16
+ 1 = fail (at least one error)
17
+ 2 = usage error (bad arguments, missing path)
18
+
19
+ Errors are format violations. Warnings are quality nudges.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import os
27
+ import re
28
+ import sys
29
+ from typing import Any
30
+
31
+
32
+ # Errors are spec violations; warnings are quality nudges.
33
+ SEVERITY_ERROR = "error"
34
+ SEVERITY_WARNING = "warning"
35
+
36
+ # name: lowercase letters, digits, hyphens. 1-64 chars.
37
+ NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
38
+ NAME_MAX = 64
39
+ DESCRIPTION_MAX = 1024
40
+ COMPATIBILITY_MAX = 500
41
+ BODY_MAX_LINES = 500
42
+
43
+
44
+ class FrontmatterParseError(ValueError):
45
+ """Raised when frontmatter parsing fails."""
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Frontmatter parsing (stdlib only)
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def _parse_scalar(raw: str) -> Any:
54
+ value = raw.strip()
55
+ if value == "":
56
+ return ""
57
+ if (value.startswith('"') and value.endswith('"')) or (
58
+ value.startswith("'") and value.endswith("'")
59
+ ):
60
+ return value[1:-1]
61
+ lower = value.lower()
62
+ if lower == "true":
63
+ return True
64
+ if lower == "false":
65
+ return False
66
+ if lower in {"null", "none", "~"}:
67
+ return None
68
+ if re.fullmatch(r"-?\d+", value):
69
+ return int(value)
70
+ if re.fullmatch(r"-?\d+\.\d+", value):
71
+ return float(value)
72
+ return value
73
+
74
+
75
+ def _collect_block_scalar(
76
+ lines: list[str], start_idx: int, min_indent: int, folded: bool
77
+ ) -> tuple[str, int]:
78
+ block_lines: list[str] = []
79
+ i = start_idx
80
+ while i < len(lines):
81
+ line = lines[i]
82
+ if line.strip() == "":
83
+ block_lines.append("")
84
+ i += 1
85
+ continue
86
+ indent = len(line) - len(line.lstrip(" "))
87
+ if indent < min_indent:
88
+ break
89
+ block_lines.append(line[min_indent:])
90
+ i += 1
91
+
92
+ if not folded:
93
+ return ("\n".join(block_lines)).rstrip(), i
94
+
95
+ paragraphs: list[str] = []
96
+ current: list[str] = []
97
+ for line in block_lines:
98
+ if line == "":
99
+ if current:
100
+ paragraphs.append(" ".join(current))
101
+ current = []
102
+ paragraphs.append("")
103
+ continue
104
+ current.append(line.strip())
105
+ if current:
106
+ paragraphs.append(" ".join(current))
107
+ return ("\n".join(paragraphs)).rstrip(), i
108
+
109
+
110
+ def _parse_frontmatter_stdlib(frontmatter_raw: str) -> dict[str, Any]:
111
+ """
112
+ Parse a conservative subset of YAML with stdlib only.
113
+ Supports: top-level key:value, one-level nested mapping, block scalars (| and >).
114
+ """
115
+ lines = frontmatter_raw.splitlines()
116
+ data: dict[str, Any] = {}
117
+ i = 0
118
+ top_key_pattern = re.compile(r"^([A-Za-z0-9_-]+)\s*:\s*(.*)$")
119
+ block_markers = {"|", ">", "|-", ">-"}
120
+
121
+ while i < len(lines):
122
+ line = lines[i]
123
+ stripped = line.strip()
124
+ if stripped == "" or stripped.startswith("#"):
125
+ i += 1
126
+ continue
127
+ if line.startswith(" "):
128
+ raise FrontmatterParseError(
129
+ f"Unexpected top-level indentation near line {i + 1}: {line}"
130
+ )
131
+
132
+ top_match = top_key_pattern.match(line)
133
+ if not top_match:
134
+ raise FrontmatterParseError(
135
+ f"Invalid top-level entry near line {i + 1}: {line}"
136
+ )
137
+ key, raw_value = top_match.group(1), top_match.group(2).strip()
138
+
139
+ if raw_value in block_markers:
140
+ folded = raw_value.startswith(">")
141
+ parsed_block, next_idx = _collect_block_scalar(lines, i + 1, min_indent=2, folded=folded)
142
+ data[key] = parsed_block
143
+ i = next_idx
144
+ continue
145
+
146
+ if raw_value != "":
147
+ if raw_value.startswith("- "):
148
+ raise FrontmatterParseError(
149
+ f"List syntax requires PyYAML near line {i + 1}."
150
+ )
151
+ data[key] = _parse_scalar(raw_value)
152
+ i += 1
153
+ continue
154
+
155
+ # raw_value == "" — possibly nested mapping
156
+ j = i + 1
157
+ while j < len(lines):
158
+ candidate = lines[j]
159
+ if candidate.strip() == "" or candidate.strip().startswith("#"):
160
+ j += 1
161
+ continue
162
+ if not candidate.startswith(" "):
163
+ break
164
+ j += 1
165
+
166
+ nested_lines = lines[i + 1 : j]
167
+ if not any(nl.strip() and not nl.strip().startswith("#") for nl in nested_lines):
168
+ data[key] = ""
169
+ i = j
170
+ continue
171
+
172
+ nested_data: dict[str, Any] = {}
173
+ k = i + 1
174
+ while k < j:
175
+ nested_line = lines[k]
176
+ ns = nested_line.strip()
177
+ if ns == "" or ns.startswith("#"):
178
+ k += 1
179
+ continue
180
+ if not nested_line.startswith(" "):
181
+ raise FrontmatterParseError(
182
+ f"Invalid nested indentation near line {k + 1}: {nested_line}"
183
+ )
184
+ content = nested_line[2:]
185
+ if content.startswith(" "):
186
+ raise FrontmatterParseError(
187
+ f"Deep nesting requires PyYAML near line {k + 1}."
188
+ )
189
+ if content.startswith("- "):
190
+ raise FrontmatterParseError(
191
+ f"List syntax requires PyYAML near line {k + 1}."
192
+ )
193
+ nested_match = top_key_pattern.match(content)
194
+ if not nested_match:
195
+ raise FrontmatterParseError(
196
+ f"Invalid nested mapping near line {k + 1}: {nested_line}"
197
+ )
198
+ child_key, child_raw = nested_match.group(1), nested_match.group(2).strip()
199
+ if child_raw in block_markers:
200
+ folded = child_raw.startswith(">")
201
+ child_block, next_k = _collect_block_scalar(
202
+ lines, k + 1, min_indent=4, folded=folded
203
+ )
204
+ nested_data[child_key] = child_block
205
+ k = next_k
206
+ continue
207
+ nested_data[child_key] = _parse_scalar(child_raw)
208
+ k += 1
209
+
210
+ data[key] = nested_data
211
+ i = j
212
+
213
+ return data
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Validation checks
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ def check_skill(skill_path: str) -> dict:
222
+ results: dict = {
223
+ "path": skill_path,
224
+ "checks": [],
225
+ "passed": 0,
226
+ "failed": 0,
227
+ "warnings": 0,
228
+ "next_steps": [],
229
+ }
230
+
231
+ def add(name: str, passed: bool, message: str, severity: str = SEVERITY_ERROR):
232
+ results["checks"].append(
233
+ {"name": name, "passed": passed, "message": message, "severity": severity}
234
+ )
235
+ if passed:
236
+ results["passed"] += 1
237
+ elif severity == SEVERITY_WARNING:
238
+ results["warnings"] += 1
239
+ else:
240
+ results["failed"] += 1
241
+
242
+ # 1. Folder exists
243
+ if not os.path.isdir(skill_path):
244
+ add("folder_exists", False, f"Path is not a directory: {skill_path}")
245
+ results["summary"] = "FAIL — folder not found"
246
+ return results
247
+ add("folder_exists", True, "Skill folder exists")
248
+
249
+ # 2. Folder name is kebab-case (a-z, 0-9, single hyphens, not at edges)
250
+ folder_name = os.path.basename(os.path.normpath(skill_path))
251
+ is_folder_kebab = bool(NAME_PATTERN.match(folder_name)) and 1 <= len(folder_name) <= NAME_MAX
252
+ add(
253
+ "folder_kebab_case",
254
+ is_folder_kebab,
255
+ f"Folder name '{folder_name}' "
256
+ f"{'is' if is_folder_kebab else 'is NOT'} valid (lowercase a-z, 0-9, single hyphens, {NAME_MAX} chars max).",
257
+ )
258
+
259
+ entries = os.listdir(skill_path)
260
+
261
+ # 3. SKILL.md exists (exact casing)
262
+ has_skill_md = "SKILL.md" in entries
263
+ add("skill_md_exists", has_skill_md, "SKILL.md exists" if has_skill_md else "SKILL.md not found")
264
+ wrong_casings = [e for e in entries if e.lower() == "skill.md" and e != "SKILL.md"]
265
+ if wrong_casings:
266
+ add(
267
+ "skill_md_casing",
268
+ False,
269
+ f"Wrong casing: '{wrong_casings[0]}' (must be exactly 'SKILL.md').",
270
+ )
271
+ if not has_skill_md:
272
+ results["summary"] = "FAIL — SKILL.md not found"
273
+ return results
274
+
275
+ # 4. README.md inside the skill folder (house style, not a spec violation)
276
+ has_readme = any(e.lower() == "readme.md" for e in entries)
277
+ add(
278
+ "readme_in_skill",
279
+ not has_readme,
280
+ "No README.md inside the skill folder" if not has_readme
281
+ else "README.md found inside the skill folder. The SKILL.md format allows extra files, but the convention is to keep human docs outside the skill folder (parent package README). Move it to the parent package and reference it from catalog docs.",
282
+ severity=SEVERITY_WARNING,
283
+ )
284
+
285
+ # 5. Parse frontmatter
286
+ skill_md_path = os.path.join(skill_path, "SKILL.md")
287
+ with open(skill_md_path, "r", encoding="utf-8") as f:
288
+ content = f.read()
289
+
290
+ fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
291
+ if not fm_match:
292
+ add("frontmatter_delimiters", False, "Missing or malformed '---' delimiters")
293
+ results["summary"] = "FAIL — frontmatter parse error"
294
+ return results
295
+ add("frontmatter_delimiters", True, "YAML frontmatter delimiters present")
296
+
297
+ try:
298
+ fm = _parse_frontmatter_stdlib(fm_match.group(1))
299
+ add("frontmatter_valid_yaml", True, "Frontmatter parsed (stdlib subset; install PyYAML for full YAML).")
300
+ except Exception as e:
301
+ add("frontmatter_valid_yaml", False, f"YAML parse error: {e}")
302
+ results["summary"] = "FAIL — YAML parse error"
303
+ return results
304
+
305
+ # 6. name field
306
+ name = fm.get("name")
307
+ if not name:
308
+ add("name_present", False, "Missing required 'name' field in frontmatter")
309
+ else:
310
+ name_str = str(name)
311
+ add("name_present", True, f"name: {name_str}")
312
+ is_name_kebab = bool(NAME_PATTERN.match(name_str)) and 1 <= len(name_str) <= NAME_MAX
313
+ add(
314
+ "name_kebab_case",
315
+ is_name_kebab,
316
+ f"name '{name_str}' {'is' if is_name_kebab else 'is NOT'} valid kebab-case.",
317
+ )
318
+ names_match = name_str == folder_name
319
+ add(
320
+ "name_matches_folder",
321
+ names_match,
322
+ f"name '{name_str}' {'matches' if names_match else 'does NOT match'} folder '{folder_name}'.",
323
+ severity=SEVERITY_WARNING,
324
+ )
325
+
326
+ # 7. description field
327
+ desc = fm.get("description")
328
+ if not desc:
329
+ add("description_present", False, "Missing required 'description' field in frontmatter")
330
+ else:
331
+ desc_str = str(desc).strip()
332
+ add("description_present", True, f"description present ({len(desc_str)} chars)")
333
+ add(
334
+ "description_length",
335
+ len(desc_str) <= DESCRIPTION_MAX,
336
+ f"Description length: {len(desc_str)}/{DESCRIPTION_MAX} chars",
337
+ )
338
+ has_xml = "<" in desc_str or ">" in desc_str
339
+ add(
340
+ "description_no_xml",
341
+ not has_xml,
342
+ "No XML angle brackets in description" if not has_xml
343
+ else "Description contains XML angle brackets '<>' (forbidden by spec).",
344
+ )
345
+ # Trigger guidance is a quality nudge, not a spec error.
346
+ trigger_keywords = ["use when", "use for", "use this", "when the user", "when you"]
347
+ has_triggers = any(kw in desc_str.lower() for kw in trigger_keywords)
348
+ add(
349
+ "description_has_triggers",
350
+ has_triggers,
351
+ "Description includes trigger guidance ('Use when...')" if has_triggers
352
+ else "Consider adding 'Use when...' trigger guidance to improve agent pickup.",
353
+ severity=SEVERITY_WARNING,
354
+ )
355
+
356
+ # 8. license (optional, warning if missing for catalog publishing)
357
+ if "license" not in fm or not fm.get("license"):
358
+ add(
359
+ "license_present",
360
+ False,
361
+ "Missing optional 'license' field. Recommended for published skills (e.g. 'CC-BY-4.0').",
362
+ severity=SEVERITY_WARNING,
363
+ )
364
+ else:
365
+ add("license_present", True, f"license: {fm.get('license')}")
366
+
367
+ # 9. compatibility (optional, length check)
368
+ compat = fm.get("compatibility")
369
+ if compat:
370
+ compat_str = str(compat)
371
+ add(
372
+ "compatibility_length",
373
+ len(compat_str) <= COMPATIBILITY_MAX,
374
+ f"compatibility length: {len(compat_str)}/{COMPATIBILITY_MAX} chars",
375
+ )
376
+
377
+ # 10. metadata (optional)
378
+ metadata = fm.get("metadata")
379
+ if metadata and isinstance(metadata, dict):
380
+ if "version" not in metadata:
381
+ add(
382
+ "metadata_version",
383
+ False,
384
+ "metadata.version not set (recommended for catalog publishing).",
385
+ severity=SEVERITY_WARNING,
386
+ )
387
+ else:
388
+ add("metadata_version", True, f"metadata.version: {metadata.get('version')}")
389
+ else:
390
+ add(
391
+ "metadata_present",
392
+ False,
393
+ "No metadata block. Recommended: metadata.version and metadata.author.",
394
+ severity=SEVERITY_WARNING,
395
+ )
396
+
397
+ # 11. Body content checks
398
+ body = content[fm_match.end():]
399
+ body_line_count = len(body.strip().split("\n"))
400
+ add(
401
+ "body_line_count",
402
+ body_line_count <= BODY_MAX_LINES,
403
+ f"SKILL.md body: {body_line_count} lines "
404
+ f"{'(good)' if body_line_count <= BODY_MAX_LINES else f'(>{BODY_MAX_LINES} — consider moving detail to references/)'}",
405
+ severity=SEVERITY_WARNING if body_line_count > BODY_MAX_LINES else SEVERITY_ERROR,
406
+ )
407
+ has_examples = bool(re.search(r"(?i)(example|user says|trigger phrase|use case)", body))
408
+ add(
409
+ "body_has_examples",
410
+ has_examples,
411
+ "Body includes examples" if has_examples else "Consider adding usage examples.",
412
+ severity=SEVERITY_WARNING,
413
+ )
414
+
415
+ # 12. Optional dirs and link integrity
416
+ for dirname in ("references", "scripts", "assets"):
417
+ dir_path = os.path.join(skill_path, dirname)
418
+ if os.path.isdir(dir_path):
419
+ for entry in os.listdir(dir_path):
420
+ ref_token = f"{dirname}/{entry}"
421
+ if entry in body or ref_token in body:
422
+ add(f"linked:{ref_token}", True, f"{ref_token} is referenced from SKILL.md")
423
+ else:
424
+ add(
425
+ f"linked:{ref_token}",
426
+ False,
427
+ f"{ref_token} exists but is not referenced from SKILL.md. "
428
+ f"Either reference it with a clear 'when to load' clause or remove it.",
429
+ severity=SEVERITY_WARNING,
430
+ )
431
+
432
+ # 13. summary
433
+ if results["failed"] == 0:
434
+ results["summary"] = (
435
+ f"PASS — {results['passed']} checks passed"
436
+ + (f", {results['warnings']} warnings" if results["warnings"] else "")
437
+ )
438
+ else:
439
+ results["summary"] = (
440
+ f"FAIL — {results['failed']} errors, {results['warnings']} warnings"
441
+ )
442
+
443
+ if results["failed"] > 0:
444
+ results["next_steps"] = [
445
+ f"Fix '{c['name']}': {c['message']}"
446
+ for c in results["checks"]
447
+ if (not c["passed"] and c["severity"] == SEVERITY_ERROR)
448
+ ]
449
+
450
+ return results
451
+
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # Reporting
455
+ # ---------------------------------------------------------------------------
456
+
457
+
458
+ def print_report(results: dict, verbose: bool = False) -> None:
459
+ print(f"\n{'=' * 64}")
460
+ print(" Skill Validation Report (open SKILL.md format)")
461
+ print(f" Path: {results['path']}")
462
+ print(f"{'=' * 64}\n")
463
+ for c in results["checks"]:
464
+ if c["passed"] and not verbose:
465
+ continue
466
+ icon = "✅" if c["passed"] else ("⚠️ " if c["severity"] == SEVERITY_WARNING else "❌")
467
+ print(f" {icon} {c['name']}: {c['message']}")
468
+ print(f"\n{'-' * 64}")
469
+ print(f" {results['summary']}")
470
+ print(f" Passed: {results['passed']} | Failed: {results['failed']} | Warnings: {results['warnings']}")
471
+ print(f"{'-' * 64}\n")
472
+ if results.get("next_steps"):
473
+ print(" Next steps:")
474
+ for i, step in enumerate(results["next_steps"], start=1):
475
+ print(f" {i}. {step}")
476
+ print()
477
+
478
+
479
+ # ---------------------------------------------------------------------------
480
+ # CLI
481
+ # ---------------------------------------------------------------------------
482
+
483
+
484
+ def main(argv: list[str] | None = None) -> int:
485
+ parser = argparse.ArgumentParser(
486
+ prog="validate.py",
487
+ description="Validate a skill folder against the open SKILL.md format.",
488
+ )
489
+ parser.add_argument("path", help="Path to the skill folder containing SKILL.md")
490
+ parser.add_argument(
491
+ "--format",
492
+ choices=["human", "json", "both"],
493
+ default="human",
494
+ help="Output format (default: human).",
495
+ )
496
+ parser.add_argument("--verbose", action="store_true", help="Include passed checks in human output.")
497
+ parser.add_argument("--pretty-json", action="store_true", help="Pretty-print JSON output.")
498
+ parser.add_argument(
499
+ "--json-out",
500
+ metavar="FILE",
501
+ help="Write JSON report to FILE (reusable for downstream agentic checks).",
502
+ )
503
+ args = parser.parse_args(argv)
504
+
505
+ if not os.path.isdir(args.path):
506
+ print(f"Error: not a directory: {args.path}", file=sys.stderr)
507
+ return 2
508
+
509
+ results = check_skill(args.path)
510
+
511
+ if args.format in {"human", "both"}:
512
+ print_report(results, verbose=args.verbose)
513
+ if not args.json_out:
514
+ print(" Tip: add --json-out FILE to reuse this report without re-running.\n")
515
+
516
+ if args.format in {"json", "both"}:
517
+ indent = 2 if args.pretty_json else None
518
+ separators = None if args.pretty_json else (",", ":")
519
+ report_json = json.dumps(results, indent=indent, separators=separators)
520
+ if args.format == "both":
521
+ print("--- JSON Report ---")
522
+ print(report_json)
523
+
524
+ if args.json_out:
525
+ indent = 2 if args.pretty_json else None
526
+ separators = None if args.pretty_json else (",", ":")
527
+ with open(args.json_out, "w", encoding="utf-8") as f:
528
+ json.dump(results, f, indent=indent, separators=separators)
529
+ if args.format in {"human", "both"}:
530
+ print(f" JSON report saved to: {args.json_out}")
531
+
532
+ return 0 if results["failed"] == 0 else 1
533
+
534
+
535
+ if __name__ == "__main__":
536
+ sys.exit(main())
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "spec-driven",
3
+ "version": "5.0.0",
4
+ "type": "skill",
5
+ "description": "Feature planning with 4 adaptive phases (Specify, Design, Tasks, Execute) + Verifier (author != verifier) + self-improving lessons layer",
6
+ "phases": ["specify", "design", "tasks", "execute", "validate", "memory", "lessons"],
7
+ "trigger": ["/spec", "/explore", "/plan", "/apply"],
8
+ "scope": "public",
9
+ "audience": "development-teams",
10
+ "dependencies": {
11
+ "required": [],
12
+ "optional": ["mermaid-studio", "codenavi"]
13
+ }
14
+ }