@phuc1403/musketeer 0.8.0 → 0.10.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 (236) hide show
  1. package/INSTALLATION.md +52 -52
  2. package/README.md +49 -49
  3. package/bin/musketeer.js +168 -168
  4. package/manifest.json +333 -301
  5. package/package.json +48 -48
  6. package/src/dotnet-scaffold-copier.js +79 -79
  7. package/src/provisioner/detect.js +93 -93
  8. package/src/self-update.js +77 -77
  9. package/template/.claude/agents/code-reviewer.md +182 -166
  10. package/template/.claude/agents/git-manager.md +18 -18
  11. package/template/.claude/agents/hallmark-auditor.md +78 -78
  12. package/template/.claude/agents/researcher.md +33 -33
  13. package/template/.claude/hooks/block-unsafe-adr-title.cjs +85 -85
  14. package/template/.claude/hooks/git-skill-reminder.cjs +53 -0
  15. package/template/.claude/hooks/init-adr-dir.cjs +173 -173
  16. package/template/.claude/hooks/inject-adr-flags.cjs +94 -94
  17. package/template/.claude/hooks/lib/adr/command-scan.cjs +115 -115
  18. package/template/.claude/hooks/lib/characteristics/checker.cjs +357 -357
  19. package/template/.claude/hooks/lib/colors.cjs +180 -122
  20. package/template/.claude/hooks/lib/git-info-cache.cjs +191 -191
  21. package/template/.claude/hooks/lib/transcript-parser.cjs +300 -277
  22. package/template/.claude/hooks/sync-adr-toc.cjs +146 -146
  23. package/template/.claude/hooks/{usage-context-awareness.cjs → usage-quota-cache-refresh.cjs} +166 -166
  24. package/template/.claude/hooks/validate-characteristics-hook.cjs +66 -66
  25. package/template/.claude/hooks/validate-cml-hook.js +145 -145
  26. package/template/.claude/skills/adr-writer/SKILL.md +48 -48
  27. package/template/.claude/skills/adr-writer/references/adr-example.md +35 -35
  28. package/template/.claude/skills/architecture-characteristic-writer/SKILL.md +215 -215
  29. package/template/.claude/skills/architecture-characteristic-writer/assets/worksheet-template.md +29 -29
  30. package/template/.claude/skills/architecture-characteristic-writer/references/characteristics-catalog.md +40 -40
  31. package/template/.claude/skills/architecture-characteristic-writer/scripts/ranking-table.cjs +171 -171
  32. package/template/.claude/skills/code-review/SKILL.md +201 -54
  33. package/template/.claude/skills/code-review/references/checklist-workflow.md +96 -0
  34. package/template/.claude/skills/code-review/references/checklists/api.md +52 -52
  35. package/template/.claude/skills/code-review/references/checklists/base.md +100 -100
  36. package/template/.claude/skills/code-review/references/checklists/web-app.md +54 -54
  37. package/template/.claude/skills/code-review/references/code-review-reception.md +113 -0
  38. package/template/.claude/skills/code-review/references/codebase-scan-workflow.md +30 -0
  39. package/template/.claude/skills/code-review/references/edge-case-scouting.md +119 -0
  40. package/template/.claude/skills/code-review/references/input-mode-resolution.md +135 -0
  41. package/template/.claude/skills/code-review/references/parallel-review-workflow.md +76 -0
  42. package/template/.claude/skills/code-review/references/requesting-code-review.md +116 -0
  43. package/template/.claude/skills/code-review/references/spec-compliance-review.md +43 -0
  44. package/template/.claude/skills/code-review/references/task-management-reviews.md +140 -0
  45. package/template/.claude/skills/code-review/references/verification-before-completion.md +139 -0
  46. package/template/.claude/skills/context-map/SKILL.md +80 -80
  47. package/template/.claude/skills/context-map/example.cml +106 -106
  48. package/template/.claude/skills/context-map/reference/Bounded Context/Bounded Context.md +40 -40
  49. package/template/.claude/skills/context-map/reference/Bounded Context/businessModel.md +5 -5
  50. package/template/.claude/skills/context-map/reference/Bounded Context/domainVisionStatement.md +2 -2
  51. package/template/.claude/skills/context-map/reference/Bounded Context/evolution.md +5 -5
  52. package/template/.claude/skills/context-map/reference/Bounded Context/implementationTechnology.md +1 -1
  53. package/template/.claude/skills/context-map/reference/Bounded Context/implements.md +1 -1
  54. package/template/.claude/skills/context-map/reference/Bounded Context/knowledgeLevel.md +4 -4
  55. package/template/.claude/skills/context-map/reference/Bounded Context/realizes.md +9 -9
  56. package/template/.claude/skills/context-map/reference/Bounded Context/refines.md +10 -10
  57. package/template/.claude/skills/context-map/reference/Bounded Context/responsibilities.md +26 -26
  58. package/template/.claude/skills/context-map/reference/Bounded Context/type.md +23 -23
  59. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Anticorruption Layer.md +5 -5
  60. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Bounded Context Relationship.md +12 -12
  61. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Conformist.md +5 -5
  62. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Customer-Supplier (C-S).md +22 -22
  63. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Open Host Service.md +4 -4
  64. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Partnership (P).md +13 -13
  65. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Published Language.md +4 -4
  66. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Shared Kernel (SK).md +12 -12
  67. package/template/.claude/skills/context-map/reference/Context Map.md +62 -62
  68. package/template/.claude/skills/context-map/reference/Domain/Domain.md +30 -30
  69. package/template/.claude/skills/context-map/reference/Domain/supports.md +33 -33
  70. package/template/.claude/skills/context-map/reference/Domain/type.md +3 -3
  71. package/template/.claude/skills/context-map/reference/Semantic Rules.md +32 -32
  72. package/template/.claude/skills/git/SKILL.md +131 -115
  73. package/template/.claude/skills/git/references/branch-management.md +88 -88
  74. package/template/.claude/skills/git/references/commit-standards.md +46 -46
  75. package/template/.claude/skills/git/references/context-efficiency.md +54 -0
  76. package/template/.claude/skills/git/references/gh-cli-guide.md +109 -109
  77. package/template/.claude/skills/git/references/safety-protocols.md +69 -69
  78. package/template/.claude/skills/git/references/workflow-commit.md +58 -58
  79. package/template/.claude/skills/git/references/workflow-merge-pr.md +136 -0
  80. package/template/.claude/skills/git/references/workflow-merge.md +48 -48
  81. package/template/.claude/skills/git/references/workflow-pr.md +58 -58
  82. package/template/.claude/skills/git/references/workflow-push.md +52 -52
  83. package/template/.claude/skills/hallmark/SKILL.md +552 -552
  84. package/template/.claude/skills/hallmark/references/anti-patterns.md +412 -412
  85. package/template/.claude/skills/hallmark/references/assets.md +406 -406
  86. package/template/.claude/skills/hallmark/references/color.md +95 -95
  87. package/template/.claude/skills/hallmark/references/component-cookbook.md +256 -256
  88. package/template/.claude/skills/hallmark/references/components/c1-outlined-chip.md +12 -12
  89. package/template/.claude/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -16
  90. package/template/.claude/skills/hallmark/references/components/c3-typographic-link.md +8 -8
  91. package/template/.claude/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -16
  92. package/template/.claude/skills/hallmark/references/components/f1-bento-grid.md +20 -20
  93. package/template/.claude/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -20
  94. package/template/.claude/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -11
  95. package/template/.claude/skills/hallmark/references/components/f4-step-sequence.md +11 -11
  96. package/template/.claude/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -11
  97. package/template/.claude/skills/hallmark/references/components/f6-product-card-grid.md +41 -41
  98. package/template/.claude/skills/hallmark/references/components/ft1-mast-headed.md +13 -13
  99. package/template/.claude/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -10
  100. package/template/.claude/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -12
  101. package/template/.claude/skills/hallmark/references/components/ft4-dense-typographic.md +10 -10
  102. package/template/.claude/skills/hallmark/references/components/ft5-statement.md +21 -21
  103. package/template/.claude/skills/hallmark/references/components/ft6-letter-close.md +19 -19
  104. package/template/.claude/skills/hallmark/references/components/ft7-newsletter-first.md +27 -27
  105. package/template/.claude/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -25
  106. package/template/.claude/skills/hallmark/references/components/h1-marquee.md +15 -15
  107. package/template/.claude/skills/hallmark/references/components/h2-split-diptych.md +15 -15
  108. package/template/.claude/skills/hallmark/references/components/h3-quote-led.md +11 -11
  109. package/template/.claude/skills/hallmark/references/components/h4-stat-led.md +14 -14
  110. package/template/.claude/skills/hallmark/references/components/h5-letter-hero.md +11 -11
  111. package/template/.claude/skills/hallmark/references/components/h6-photographic-fold.md +16 -16
  112. package/template/.claude/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -27
  113. package/template/.claude/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -23
  114. package/template/.claude/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -27
  115. package/template/.claude/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -12
  116. package/template/.claude/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -19
  117. package/template/.claude/skills/hallmark/references/components/n2-floating-chip.md +14 -14
  118. package/template/.claude/skills/hallmark/references/components/n3-side-rail.md +14 -14
  119. package/template/.claude/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -9
  120. package/template/.claude/skills/hallmark/references/components/n5-floating-pill.md +28 -28
  121. package/template/.claude/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -24
  122. package/template/.claude/skills/hallmark/references/components/n7-brutal-slab.md +22 -22
  123. package/template/.claude/skills/hallmark/references/components/n8-terminal-command.md +21 -21
  124. package/template/.claude/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -17
  125. package/template/.claude/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -15
  126. package/template/.claude/skills/hallmark/references/components/s2-hanging.md +13 -13
  127. package/template/.claude/skills/hallmark/references/components/s3-sticky-pinned.md +19 -19
  128. package/template/.claude/skills/hallmark/references/components/s4-inline-no-break.md +11 -11
  129. package/template/.claude/skills/hallmark/references/components/s5-bottom-anchored.md +13 -13
  130. package/template/.claude/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -12
  131. package/template/.claude/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -19
  132. package/template/.claude/skills/hallmark/references/components/t3-single-huge-quote.md +11 -11
  133. package/template/.claude/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -14
  134. package/template/.claude/skills/hallmark/references/contract.md +24 -24
  135. package/template/.claude/skills/hallmark/references/copy.md +182 -182
  136. package/template/.claude/skills/hallmark/references/custom-craft.md +626 -626
  137. package/template/.claude/skills/hallmark/references/custom-theme.md +329 -329
  138. package/template/.claude/skills/hallmark/references/design-md.md +116 -116
  139. package/template/.claude/skills/hallmark/references/export-formats.md +328 -328
  140. package/template/.claude/skills/hallmark/references/floating-nav.md +89 -89
  141. package/template/.claude/skills/hallmark/references/genres/atmospheric.md +65 -65
  142. package/template/.claude/skills/hallmark/references/genres/editorial.md +70 -70
  143. package/template/.claude/skills/hallmark/references/genres/modern-minimal.md +67 -67
  144. package/template/.claude/skills/hallmark/references/genres/playful.md +65 -65
  145. package/template/.claude/skills/hallmark/references/hero-enrichment.md +474 -474
  146. package/template/.claude/skills/hallmark/references/imagery-kit.md +170 -170
  147. package/template/.claude/skills/hallmark/references/interaction-and-states.md +207 -207
  148. package/template/.claude/skills/hallmark/references/layout-and-space.md +111 -111
  149. package/template/.claude/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -35
  150. package/template/.claude/skills/hallmark/references/macrostructures/02-long-document.md +34 -34
  151. package/template/.claude/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -31
  152. package/template/.claude/skills/hallmark/references/macrostructures/04-stat-led.md +32 -32
  153. package/template/.claude/skills/hallmark/references/macrostructures/05-workbench.md +32 -32
  154. package/template/.claude/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -33
  155. package/template/.claude/skills/hallmark/references/macrostructures/07-manifesto.md +32 -32
  156. package/template/.claude/skills/hallmark/references/macrostructures/08-photographic.md +34 -34
  157. package/template/.claude/skills/hallmark/references/macrostructures/09-quote-led.md +32 -32
  158. package/template/.claude/skills/hallmark/references/macrostructures/10-specimen.md +32 -32
  159. package/template/.claude/skills/hallmark/references/macrostructures/11-catalogue.md +23 -23
  160. package/template/.claude/skills/hallmark/references/macrostructures/12-letter.md +23 -23
  161. package/template/.claude/skills/hallmark/references/macrostructures/13-index-first.md +23 -23
  162. package/template/.claude/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -23
  163. package/template/.claude/skills/hallmark/references/macrostructures/15-split-studio.md +23 -23
  164. package/template/.claude/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -23
  165. package/template/.claude/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -23
  166. package/template/.claude/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -23
  167. package/template/.claude/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -23
  168. package/template/.claude/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -23
  169. package/template/.claude/skills/hallmark/references/macrostructures/21-component-playground.md +23 -23
  170. package/template/.claude/skills/hallmark/references/macrostructures.md +89 -89
  171. package/template/.claude/skills/hallmark/references/microinteractions.md +260 -260
  172. package/template/.claude/skills/hallmark/references/motion.md +109 -109
  173. package/template/.claude/skills/hallmark/references/preview-examples.md +49 -49
  174. package/template/.claude/skills/hallmark/references/responsive.md +138 -138
  175. package/template/.claude/skills/hallmark/references/slop-test.md +205 -205
  176. package/template/.claude/skills/hallmark/references/structure.md +164 -164
  177. package/template/.claude/skills/hallmark/references/study.md +511 -511
  178. package/template/.claude/skills/hallmark/references/typography.md +243 -243
  179. package/template/.claude/skills/hallmark/references/verbs/audit.md +25 -25
  180. package/template/.claude/skills/hallmark/references/verbs/redesign.md +269 -269
  181. package/template/.claude/skills/hallmark-loop/SKILL.md +105 -105
  182. package/template/.claude/skills/hallmark-loop/references/auditor-call.md +60 -60
  183. package/template/.claude/skills/hallmark-loop/references/capture.md +78 -78
  184. package/template/.claude/skills/hallmark-loop/references/loop-control.md +79 -79
  185. package/template/.claude/skills/handoff/SKILL.md +15 -15
  186. package/template/.claude/skills/knowledge-crunching/SKILL.md +94 -94
  187. package/template/.claude/skills/research/SKILL.md +69 -69
  188. package/template/.claude/skills/skill-creator/LICENSE.txt +201 -201
  189. package/template/.claude/skills/skill-creator/SKILL.md +154 -149
  190. package/template/.claude/skills/skill-creator/agents/analyzer.md +274 -274
  191. package/template/.claude/skills/skill-creator/agents/comparator.md +202 -202
  192. package/template/.claude/skills/skill-creator/agents/grader.md +223 -223
  193. package/template/.claude/skills/skill-creator/assets/eval_review.html +146 -146
  194. package/template/.claude/skills/skill-creator/eval-viewer/generate_review.py +471 -471
  195. package/template/.claude/skills/skill-creator/eval-viewer/viewer.html +1325 -1325
  196. package/template/.claude/skills/skill-creator/references/benchmark-optimization-guide.md +86 -86
  197. package/template/.claude/skills/skill-creator/references/distribution-guide.md +79 -79
  198. package/template/.claude/skills/skill-creator/references/eval-infrastructure-guide.md +129 -129
  199. package/template/.claude/skills/skill-creator/references/eval-schemas.md +121 -121
  200. package/template/.claude/skills/skill-creator/references/mcp-skills-integration.md +71 -71
  201. package/template/.claude/skills/skill-creator/references/metadata-quality-criteria.md +94 -94
  202. package/template/.claude/skills/skill-creator/references/plugin-marketplace-hosting.md +104 -104
  203. package/template/.claude/skills/skill-creator/references/plugin-marketplace-overview.md +89 -89
  204. package/template/.claude/skills/skill-creator/references/plugin-marketplace-schema.md +93 -93
  205. package/template/.claude/skills/skill-creator/references/plugin-marketplace-sources.md +103 -103
  206. package/template/.claude/skills/skill-creator/references/plugin-marketplace-troubleshooting.md +76 -76
  207. package/template/.claude/skills/skill-creator/references/script-quality-criteria.md +106 -106
  208. package/template/.claude/skills/skill-creator/references/skill-anatomy-and-requirements.md +77 -77
  209. package/template/.claude/skills/skill-creator/references/skill-creation-workflow.md +152 -151
  210. package/template/.claude/skills/skill-creator/references/skill-design-patterns.md +75 -75
  211. package/template/.claude/skills/skill-creator/references/skillmark-benchmark-criteria.md +102 -102
  212. package/template/.claude/skills/skill-creator/references/structure-organization-criteria.md +114 -114
  213. package/template/.claude/skills/skill-creator/references/testing-and-iteration.md +78 -78
  214. package/template/.claude/skills/skill-creator/references/token-efficiency-criteria.md +74 -74
  215. package/template/.claude/skills/skill-creator/references/troubleshooting-guide.md +81 -81
  216. package/template/.claude/skills/skill-creator/references/validation-checklist.md +83 -83
  217. package/template/.claude/skills/skill-creator/references/writing-effective-instructions.md +88 -88
  218. package/template/.claude/skills/skill-creator/references/yaml-frontmatter-reference.md +92 -92
  219. package/template/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +401 -401
  220. package/template/.claude/skills/skill-creator/scripts/encoding_utils.py +36 -36
  221. package/template/.claude/skills/skill-creator/scripts/generate_report.py +326 -326
  222. package/template/.claude/skills/skill-creator/scripts/improve_description.py +248 -248
  223. package/template/.claude/skills/skill-creator/scripts/init_skill.py +360 -360
  224. package/template/.claude/skills/skill-creator/scripts/package_skill.py +143 -143
  225. package/template/.claude/skills/skill-creator/scripts/quick_validate.py +110 -110
  226. package/template/.claude/skills/skill-creator/scripts/run_eval.py +310 -310
  227. package/template/.claude/skills/skill-creator/scripts/run_loop.py +332 -332
  228. package/template/.claude/skills/skill-creator/scripts/utils.py +47 -47
  229. package/template/.claude/skills/tdd/SKILL.md +142 -142
  230. package/template/.claude/skills/tdd/deep-modules.md +15 -15
  231. package/template/.claude/skills/tdd/interface-design.md +31 -31
  232. package/template/.claude/skills/tdd/mocking.md +59 -59
  233. package/template/.claude/skills/tdd/refactoring.md +10 -10
  234. package/template/.claude/skills/tdd/tests.md +61 -61
  235. package/template/.claude/statusline.cjs +0 -0
  236. package/template/.claude/skills/code-review/references/adversarial-review.md +0 -223
@@ -1,401 +1,401 @@
1
- #!/usr/bin/env python3
2
- """
3
- Aggregate individual run results into benchmark summary statistics.
4
-
5
- Reads grading.json files from run directories and produces:
6
- - run_summary with mean, stddev, min, max for each metric
7
- - delta between with_skill and without_skill configurations
8
-
9
- Usage:
10
- python aggregate_benchmark.py <benchmark_dir>
11
-
12
- Example:
13
- python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/
14
-
15
- The script supports two directory layouts:
16
-
17
- Workspace layout (from skill-creator iterations):
18
- <benchmark_dir>/
19
- └── eval-N/
20
- ├── with_skill/
21
- │ ├── run-1/grading.json
22
- │ └── run-2/grading.json
23
- └── without_skill/
24
- ├── run-1/grading.json
25
- └── run-2/grading.json
26
-
27
- Legacy layout (with runs/ subdirectory):
28
- <benchmark_dir>/
29
- └── runs/
30
- └── eval-N/
31
- ├── with_skill/
32
- │ └── run-1/grading.json
33
- └── without_skill/
34
- └── run-1/grading.json
35
- """
36
-
37
- import argparse
38
- import json
39
- import math
40
- import sys
41
- from datetime import datetime, timezone
42
- from pathlib import Path
43
-
44
-
45
- def calculate_stats(values: list[float]) -> dict:
46
- """Calculate mean, stddev, min, max for a list of values."""
47
- if not values:
48
- return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}
49
-
50
- n = len(values)
51
- mean = sum(values) / n
52
-
53
- if n > 1:
54
- variance = sum((x - mean) ** 2 for x in values) / (n - 1)
55
- stddev = math.sqrt(variance)
56
- else:
57
- stddev = 0.0
58
-
59
- return {
60
- "mean": round(mean, 4),
61
- "stddev": round(stddev, 4),
62
- "min": round(min(values), 4),
63
- "max": round(max(values), 4)
64
- }
65
-
66
-
67
- def load_run_results(benchmark_dir: Path) -> dict:
68
- """
69
- Load all run results from a benchmark directory.
70
-
71
- Returns dict keyed by config name (e.g. "with_skill"/"without_skill",
72
- or "new_skill"/"old_skill"), each containing a list of run results.
73
- """
74
- # Support both layouts: eval dirs directly under benchmark_dir, or under runs/
75
- runs_dir = benchmark_dir / "runs"
76
- if runs_dir.exists():
77
- search_dir = runs_dir
78
- elif list(benchmark_dir.glob("eval-*")):
79
- search_dir = benchmark_dir
80
- else:
81
- print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}")
82
- return {}
83
-
84
- results: dict[str, list] = {}
85
-
86
- for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))):
87
- metadata_path = eval_dir / "eval_metadata.json"
88
- if metadata_path.exists():
89
- try:
90
- with open(metadata_path) as mf:
91
- eval_id = json.load(mf).get("eval_id", eval_idx)
92
- except (json.JSONDecodeError, OSError):
93
- eval_id = eval_idx
94
- else:
95
- try:
96
- eval_id = int(eval_dir.name.split("-")[1])
97
- except ValueError:
98
- eval_id = eval_idx
99
-
100
- # Discover config directories dynamically rather than hardcoding names
101
- for config_dir in sorted(eval_dir.iterdir()):
102
- if not config_dir.is_dir():
103
- continue
104
- # Skip non-config directories (inputs, outputs, etc.)
105
- if not list(config_dir.glob("run-*")):
106
- continue
107
- config = config_dir.name
108
- if config not in results:
109
- results[config] = []
110
-
111
- for run_dir in sorted(config_dir.glob("run-*")):
112
- run_number = int(run_dir.name.split("-")[1])
113
- grading_file = run_dir / "grading.json"
114
-
115
- if not grading_file.exists():
116
- print(f"Warning: grading.json not found in {run_dir}")
117
- continue
118
-
119
- try:
120
- with open(grading_file) as f:
121
- grading = json.load(f)
122
- except json.JSONDecodeError as e:
123
- print(f"Warning: Invalid JSON in {grading_file}: {e}")
124
- continue
125
-
126
- # Extract metrics
127
- result = {
128
- "eval_id": eval_id,
129
- "run_number": run_number,
130
- "pass_rate": grading.get("summary", {}).get("pass_rate", 0.0),
131
- "passed": grading.get("summary", {}).get("passed", 0),
132
- "failed": grading.get("summary", {}).get("failed", 0),
133
- "total": grading.get("summary", {}).get("total", 0),
134
- }
135
-
136
- # Extract timing — check grading.json first, then sibling timing.json
137
- timing = grading.get("timing", {})
138
- result["time_seconds"] = timing.get("total_duration_seconds", 0.0)
139
- timing_file = run_dir / "timing.json"
140
- if result["time_seconds"] == 0.0 and timing_file.exists():
141
- try:
142
- with open(timing_file) as tf:
143
- timing_data = json.load(tf)
144
- result["time_seconds"] = timing_data.get("total_duration_seconds", 0.0)
145
- result["tokens"] = timing_data.get("total_tokens", 0)
146
- except json.JSONDecodeError:
147
- pass
148
-
149
- # Extract metrics if available
150
- metrics = grading.get("execution_metrics", {})
151
- result["tool_calls"] = metrics.get("total_tool_calls", 0)
152
- if not result.get("tokens"):
153
- result["tokens"] = metrics.get("output_chars", 0)
154
- result["errors"] = metrics.get("errors_encountered", 0)
155
-
156
- # Extract expectations — viewer requires fields: text, passed, evidence
157
- raw_expectations = grading.get("expectations", [])
158
- for exp in raw_expectations:
159
- if "text" not in exp or "passed" not in exp:
160
- print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}")
161
- result["expectations"] = raw_expectations
162
-
163
- # Extract notes from user_notes_summary
164
- notes_summary = grading.get("user_notes_summary", {})
165
- notes = []
166
- notes.extend(notes_summary.get("uncertainties", []))
167
- notes.extend(notes_summary.get("needs_review", []))
168
- notes.extend(notes_summary.get("workarounds", []))
169
- result["notes"] = notes
170
-
171
- results[config].append(result)
172
-
173
- return results
174
-
175
-
176
- def aggregate_results(results: dict) -> dict:
177
- """
178
- Aggregate run results into summary statistics.
179
-
180
- Returns run_summary with stats for each configuration and delta.
181
- """
182
- run_summary = {}
183
- configs = list(results.keys())
184
-
185
- for config in configs:
186
- runs = results.get(config, [])
187
-
188
- if not runs:
189
- run_summary[config] = {
190
- "pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
191
- "time_seconds": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
192
- "tokens": {"mean": 0, "stddev": 0, "min": 0, "max": 0}
193
- }
194
- continue
195
-
196
- pass_rates = [r["pass_rate"] for r in runs]
197
- times = [r["time_seconds"] for r in runs]
198
- tokens = [r.get("tokens", 0) for r in runs]
199
-
200
- run_summary[config] = {
201
- "pass_rate": calculate_stats(pass_rates),
202
- "time_seconds": calculate_stats(times),
203
- "tokens": calculate_stats(tokens)
204
- }
205
-
206
- # Calculate delta between the first two configs (if two exist)
207
- if len(configs) >= 2:
208
- primary = run_summary.get(configs[0], {})
209
- baseline = run_summary.get(configs[1], {})
210
- else:
211
- primary = run_summary.get(configs[0], {}) if configs else {}
212
- baseline = {}
213
-
214
- delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0)
215
- delta_time = primary.get("time_seconds", {}).get("mean", 0) - baseline.get("time_seconds", {}).get("mean", 0)
216
- delta_tokens = primary.get("tokens", {}).get("mean", 0) - baseline.get("tokens", {}).get("mean", 0)
217
-
218
- run_summary["delta"] = {
219
- "pass_rate": f"{delta_pass_rate:+.2f}",
220
- "time_seconds": f"{delta_time:+.1f}",
221
- "tokens": f"{delta_tokens:+.0f}"
222
- }
223
-
224
- return run_summary
225
-
226
-
227
- def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict:
228
- """
229
- Generate complete benchmark.json from run results.
230
- """
231
- results = load_run_results(benchmark_dir)
232
- run_summary = aggregate_results(results)
233
-
234
- # Build runs array for benchmark.json
235
- runs = []
236
- for config in results:
237
- for result in results[config]:
238
- runs.append({
239
- "eval_id": result["eval_id"],
240
- "configuration": config,
241
- "run_number": result["run_number"],
242
- "result": {
243
- "pass_rate": result["pass_rate"],
244
- "passed": result["passed"],
245
- "failed": result["failed"],
246
- "total": result["total"],
247
- "time_seconds": result["time_seconds"],
248
- "tokens": result.get("tokens", 0),
249
- "tool_calls": result.get("tool_calls", 0),
250
- "errors": result.get("errors", 0)
251
- },
252
- "expectations": result["expectations"],
253
- "notes": result["notes"]
254
- })
255
-
256
- # Determine eval IDs from results
257
- eval_ids = sorted(set(
258
- r["eval_id"]
259
- for config in results.values()
260
- for r in config
261
- ))
262
-
263
- benchmark = {
264
- "metadata": {
265
- "skill_name": skill_name or "<skill-name>",
266
- "skill_path": skill_path or "<path/to/skill>",
267
- "executor_model": "<model-name>",
268
- "analyzer_model": "<model-name>",
269
- "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
270
- "evals_run": eval_ids,
271
- "runs_per_configuration": 3
272
- },
273
- "runs": runs,
274
- "run_summary": run_summary,
275
- "notes": [] # To be filled by analyzer
276
- }
277
-
278
- return benchmark
279
-
280
-
281
- def generate_markdown(benchmark: dict) -> str:
282
- """Generate human-readable benchmark.md from benchmark data."""
283
- metadata = benchmark["metadata"]
284
- run_summary = benchmark["run_summary"]
285
-
286
- # Determine config names (excluding "delta")
287
- configs = [k for k in run_summary if k != "delta"]
288
- config_a = configs[0] if len(configs) >= 1 else "config_a"
289
- config_b = configs[1] if len(configs) >= 2 else "config_b"
290
- label_a = config_a.replace("_", " ").title()
291
- label_b = config_b.replace("_", " ").title()
292
-
293
- lines = [
294
- f"# Skill Benchmark: {metadata['skill_name']}",
295
- "",
296
- f"**Model**: {metadata['executor_model']}",
297
- f"**Date**: {metadata['timestamp']}",
298
- f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({metadata['runs_per_configuration']} runs each per configuration)",
299
- "",
300
- "## Summary",
301
- "",
302
- f"| Metric | {label_a} | {label_b} | Delta |",
303
- "|--------|------------|---------------|-------|",
304
- ]
305
-
306
- a_summary = run_summary.get(config_a, {})
307
- b_summary = run_summary.get(config_b, {})
308
- delta = run_summary.get("delta", {})
309
-
310
- # Format pass rate
311
- a_pr = a_summary.get("pass_rate", {})
312
- b_pr = b_summary.get("pass_rate", {})
313
- lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |")
314
-
315
- # Format time
316
- a_time = a_summary.get("time_seconds", {})
317
- b_time = b_summary.get("time_seconds", {})
318
- lines.append(f"| Time | {a_time.get('mean', 0):.1f}s ± {a_time.get('stddev', 0):.1f}s | {b_time.get('mean', 0):.1f}s ± {b_time.get('stddev', 0):.1f}s | {delta.get('time_seconds', '—')}s |")
319
-
320
- # Format tokens
321
- a_tokens = a_summary.get("tokens", {})
322
- b_tokens = b_summary.get("tokens", {})
323
- lines.append(f"| Tokens | {a_tokens.get('mean', 0):.0f} ± {a_tokens.get('stddev', 0):.0f} | {b_tokens.get('mean', 0):.0f} ± {b_tokens.get('stddev', 0):.0f} | {delta.get('tokens', '—')} |")
324
-
325
- # Notes section
326
- if benchmark.get("notes"):
327
- lines.extend([
328
- "",
329
- "## Notes",
330
- ""
331
- ])
332
- for note in benchmark["notes"]:
333
- lines.append(f"- {note}")
334
-
335
- return "\n".join(lines)
336
-
337
-
338
- def main():
339
- parser = argparse.ArgumentParser(
340
- description="Aggregate benchmark run results into summary statistics"
341
- )
342
- parser.add_argument(
343
- "benchmark_dir",
344
- type=Path,
345
- help="Path to the benchmark directory"
346
- )
347
- parser.add_argument(
348
- "--skill-name",
349
- default="",
350
- help="Name of the skill being benchmarked"
351
- )
352
- parser.add_argument(
353
- "--skill-path",
354
- default="",
355
- help="Path to the skill being benchmarked"
356
- )
357
- parser.add_argument(
358
- "--output", "-o",
359
- type=Path,
360
- help="Output path for benchmark.json (default: <benchmark_dir>/benchmark.json)"
361
- )
362
-
363
- args = parser.parse_args()
364
-
365
- if not args.benchmark_dir.exists():
366
- print(f"Directory not found: {args.benchmark_dir}")
367
- sys.exit(1)
368
-
369
- # Generate benchmark
370
- benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path)
371
-
372
- # Determine output paths
373
- output_json = args.output or (args.benchmark_dir / "benchmark.json")
374
- output_md = output_json.with_suffix(".md")
375
-
376
- # Write benchmark.json
377
- with open(output_json, "w") as f:
378
- json.dump(benchmark, f, indent=2)
379
- print(f"Generated: {output_json}")
380
-
381
- # Write benchmark.md
382
- markdown = generate_markdown(benchmark)
383
- with open(output_md, "w") as f:
384
- f.write(markdown)
385
- print(f"Generated: {output_md}")
386
-
387
- # Print summary
388
- run_summary = benchmark["run_summary"]
389
- configs = [k for k in run_summary if k != "delta"]
390
- delta = run_summary.get("delta", {})
391
-
392
- print(f"\nSummary:")
393
- for config in configs:
394
- pr = run_summary[config]["pass_rate"]["mean"]
395
- label = config.replace("_", " ").title()
396
- print(f" {label}: {pr*100:.1f}% pass rate")
397
- print(f" Delta: {delta.get('pass_rate', '—')}")
398
-
399
-
400
- if __name__ == "__main__":
401
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ Aggregate individual run results into benchmark summary statistics.
4
+
5
+ Reads grading.json files from run directories and produces:
6
+ - run_summary with mean, stddev, min, max for each metric
7
+ - delta between with_skill and without_skill configurations
8
+
9
+ Usage:
10
+ python aggregate_benchmark.py <benchmark_dir>
11
+
12
+ Example:
13
+ python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/
14
+
15
+ The script supports two directory layouts:
16
+
17
+ Workspace layout (from skill-creator iterations):
18
+ <benchmark_dir>/
19
+ └── eval-N/
20
+ ├── with_skill/
21
+ │ ├── run-1/grading.json
22
+ │ └── run-2/grading.json
23
+ └── without_skill/
24
+ ├── run-1/grading.json
25
+ └── run-2/grading.json
26
+
27
+ Legacy layout (with runs/ subdirectory):
28
+ <benchmark_dir>/
29
+ └── runs/
30
+ └── eval-N/
31
+ ├── with_skill/
32
+ │ └── run-1/grading.json
33
+ └── without_skill/
34
+ └── run-1/grading.json
35
+ """
36
+
37
+ import argparse
38
+ import json
39
+ import math
40
+ import sys
41
+ from datetime import datetime, timezone
42
+ from pathlib import Path
43
+
44
+
45
+ def calculate_stats(values: list[float]) -> dict:
46
+ """Calculate mean, stddev, min, max for a list of values."""
47
+ if not values:
48
+ return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}
49
+
50
+ n = len(values)
51
+ mean = sum(values) / n
52
+
53
+ if n > 1:
54
+ variance = sum((x - mean) ** 2 for x in values) / (n - 1)
55
+ stddev = math.sqrt(variance)
56
+ else:
57
+ stddev = 0.0
58
+
59
+ return {
60
+ "mean": round(mean, 4),
61
+ "stddev": round(stddev, 4),
62
+ "min": round(min(values), 4),
63
+ "max": round(max(values), 4)
64
+ }
65
+
66
+
67
+ def load_run_results(benchmark_dir: Path) -> dict:
68
+ """
69
+ Load all run results from a benchmark directory.
70
+
71
+ Returns dict keyed by config name (e.g. "with_skill"/"without_skill",
72
+ or "new_skill"/"old_skill"), each containing a list of run results.
73
+ """
74
+ # Support both layouts: eval dirs directly under benchmark_dir, or under runs/
75
+ runs_dir = benchmark_dir / "runs"
76
+ if runs_dir.exists():
77
+ search_dir = runs_dir
78
+ elif list(benchmark_dir.glob("eval-*")):
79
+ search_dir = benchmark_dir
80
+ else:
81
+ print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}")
82
+ return {}
83
+
84
+ results: dict[str, list] = {}
85
+
86
+ for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))):
87
+ metadata_path = eval_dir / "eval_metadata.json"
88
+ if metadata_path.exists():
89
+ try:
90
+ with open(metadata_path) as mf:
91
+ eval_id = json.load(mf).get("eval_id", eval_idx)
92
+ except (json.JSONDecodeError, OSError):
93
+ eval_id = eval_idx
94
+ else:
95
+ try:
96
+ eval_id = int(eval_dir.name.split("-")[1])
97
+ except ValueError:
98
+ eval_id = eval_idx
99
+
100
+ # Discover config directories dynamically rather than hardcoding names
101
+ for config_dir in sorted(eval_dir.iterdir()):
102
+ if not config_dir.is_dir():
103
+ continue
104
+ # Skip non-config directories (inputs, outputs, etc.)
105
+ if not list(config_dir.glob("run-*")):
106
+ continue
107
+ config = config_dir.name
108
+ if config not in results:
109
+ results[config] = []
110
+
111
+ for run_dir in sorted(config_dir.glob("run-*")):
112
+ run_number = int(run_dir.name.split("-")[1])
113
+ grading_file = run_dir / "grading.json"
114
+
115
+ if not grading_file.exists():
116
+ print(f"Warning: grading.json not found in {run_dir}")
117
+ continue
118
+
119
+ try:
120
+ with open(grading_file) as f:
121
+ grading = json.load(f)
122
+ except json.JSONDecodeError as e:
123
+ print(f"Warning: Invalid JSON in {grading_file}: {e}")
124
+ continue
125
+
126
+ # Extract metrics
127
+ result = {
128
+ "eval_id": eval_id,
129
+ "run_number": run_number,
130
+ "pass_rate": grading.get("summary", {}).get("pass_rate", 0.0),
131
+ "passed": grading.get("summary", {}).get("passed", 0),
132
+ "failed": grading.get("summary", {}).get("failed", 0),
133
+ "total": grading.get("summary", {}).get("total", 0),
134
+ }
135
+
136
+ # Extract timing — check grading.json first, then sibling timing.json
137
+ timing = grading.get("timing", {})
138
+ result["time_seconds"] = timing.get("total_duration_seconds", 0.0)
139
+ timing_file = run_dir / "timing.json"
140
+ if result["time_seconds"] == 0.0 and timing_file.exists():
141
+ try:
142
+ with open(timing_file) as tf:
143
+ timing_data = json.load(tf)
144
+ result["time_seconds"] = timing_data.get("total_duration_seconds", 0.0)
145
+ result["tokens"] = timing_data.get("total_tokens", 0)
146
+ except json.JSONDecodeError:
147
+ pass
148
+
149
+ # Extract metrics if available
150
+ metrics = grading.get("execution_metrics", {})
151
+ result["tool_calls"] = metrics.get("total_tool_calls", 0)
152
+ if not result.get("tokens"):
153
+ result["tokens"] = metrics.get("output_chars", 0)
154
+ result["errors"] = metrics.get("errors_encountered", 0)
155
+
156
+ # Extract expectations — viewer requires fields: text, passed, evidence
157
+ raw_expectations = grading.get("expectations", [])
158
+ for exp in raw_expectations:
159
+ if "text" not in exp or "passed" not in exp:
160
+ print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}")
161
+ result["expectations"] = raw_expectations
162
+
163
+ # Extract notes from user_notes_summary
164
+ notes_summary = grading.get("user_notes_summary", {})
165
+ notes = []
166
+ notes.extend(notes_summary.get("uncertainties", []))
167
+ notes.extend(notes_summary.get("needs_review", []))
168
+ notes.extend(notes_summary.get("workarounds", []))
169
+ result["notes"] = notes
170
+
171
+ results[config].append(result)
172
+
173
+ return results
174
+
175
+
176
+ def aggregate_results(results: dict) -> dict:
177
+ """
178
+ Aggregate run results into summary statistics.
179
+
180
+ Returns run_summary with stats for each configuration and delta.
181
+ """
182
+ run_summary = {}
183
+ configs = list(results.keys())
184
+
185
+ for config in configs:
186
+ runs = results.get(config, [])
187
+
188
+ if not runs:
189
+ run_summary[config] = {
190
+ "pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
191
+ "time_seconds": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
192
+ "tokens": {"mean": 0, "stddev": 0, "min": 0, "max": 0}
193
+ }
194
+ continue
195
+
196
+ pass_rates = [r["pass_rate"] for r in runs]
197
+ times = [r["time_seconds"] for r in runs]
198
+ tokens = [r.get("tokens", 0) for r in runs]
199
+
200
+ run_summary[config] = {
201
+ "pass_rate": calculate_stats(pass_rates),
202
+ "time_seconds": calculate_stats(times),
203
+ "tokens": calculate_stats(tokens)
204
+ }
205
+
206
+ # Calculate delta between the first two configs (if two exist)
207
+ if len(configs) >= 2:
208
+ primary = run_summary.get(configs[0], {})
209
+ baseline = run_summary.get(configs[1], {})
210
+ else:
211
+ primary = run_summary.get(configs[0], {}) if configs else {}
212
+ baseline = {}
213
+
214
+ delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0)
215
+ delta_time = primary.get("time_seconds", {}).get("mean", 0) - baseline.get("time_seconds", {}).get("mean", 0)
216
+ delta_tokens = primary.get("tokens", {}).get("mean", 0) - baseline.get("tokens", {}).get("mean", 0)
217
+
218
+ run_summary["delta"] = {
219
+ "pass_rate": f"{delta_pass_rate:+.2f}",
220
+ "time_seconds": f"{delta_time:+.1f}",
221
+ "tokens": f"{delta_tokens:+.0f}"
222
+ }
223
+
224
+ return run_summary
225
+
226
+
227
+ def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict:
228
+ """
229
+ Generate complete benchmark.json from run results.
230
+ """
231
+ results = load_run_results(benchmark_dir)
232
+ run_summary = aggregate_results(results)
233
+
234
+ # Build runs array for benchmark.json
235
+ runs = []
236
+ for config in results:
237
+ for result in results[config]:
238
+ runs.append({
239
+ "eval_id": result["eval_id"],
240
+ "configuration": config,
241
+ "run_number": result["run_number"],
242
+ "result": {
243
+ "pass_rate": result["pass_rate"],
244
+ "passed": result["passed"],
245
+ "failed": result["failed"],
246
+ "total": result["total"],
247
+ "time_seconds": result["time_seconds"],
248
+ "tokens": result.get("tokens", 0),
249
+ "tool_calls": result.get("tool_calls", 0),
250
+ "errors": result.get("errors", 0)
251
+ },
252
+ "expectations": result["expectations"],
253
+ "notes": result["notes"]
254
+ })
255
+
256
+ # Determine eval IDs from results
257
+ eval_ids = sorted(set(
258
+ r["eval_id"]
259
+ for config in results.values()
260
+ for r in config
261
+ ))
262
+
263
+ benchmark = {
264
+ "metadata": {
265
+ "skill_name": skill_name or "<skill-name>",
266
+ "skill_path": skill_path or "<path/to/skill>",
267
+ "executor_model": "<model-name>",
268
+ "analyzer_model": "<model-name>",
269
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
270
+ "evals_run": eval_ids,
271
+ "runs_per_configuration": 3
272
+ },
273
+ "runs": runs,
274
+ "run_summary": run_summary,
275
+ "notes": [] # To be filled by analyzer
276
+ }
277
+
278
+ return benchmark
279
+
280
+
281
+ def generate_markdown(benchmark: dict) -> str:
282
+ """Generate human-readable benchmark.md from benchmark data."""
283
+ metadata = benchmark["metadata"]
284
+ run_summary = benchmark["run_summary"]
285
+
286
+ # Determine config names (excluding "delta")
287
+ configs = [k for k in run_summary if k != "delta"]
288
+ config_a = configs[0] if len(configs) >= 1 else "config_a"
289
+ config_b = configs[1] if len(configs) >= 2 else "config_b"
290
+ label_a = config_a.replace("_", " ").title()
291
+ label_b = config_b.replace("_", " ").title()
292
+
293
+ lines = [
294
+ f"# Skill Benchmark: {metadata['skill_name']}",
295
+ "",
296
+ f"**Model**: {metadata['executor_model']}",
297
+ f"**Date**: {metadata['timestamp']}",
298
+ f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({metadata['runs_per_configuration']} runs each per configuration)",
299
+ "",
300
+ "## Summary",
301
+ "",
302
+ f"| Metric | {label_a} | {label_b} | Delta |",
303
+ "|--------|------------|---------------|-------|",
304
+ ]
305
+
306
+ a_summary = run_summary.get(config_a, {})
307
+ b_summary = run_summary.get(config_b, {})
308
+ delta = run_summary.get("delta", {})
309
+
310
+ # Format pass rate
311
+ a_pr = a_summary.get("pass_rate", {})
312
+ b_pr = b_summary.get("pass_rate", {})
313
+ lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |")
314
+
315
+ # Format time
316
+ a_time = a_summary.get("time_seconds", {})
317
+ b_time = b_summary.get("time_seconds", {})
318
+ lines.append(f"| Time | {a_time.get('mean', 0):.1f}s ± {a_time.get('stddev', 0):.1f}s | {b_time.get('mean', 0):.1f}s ± {b_time.get('stddev', 0):.1f}s | {delta.get('time_seconds', '—')}s |")
319
+
320
+ # Format tokens
321
+ a_tokens = a_summary.get("tokens", {})
322
+ b_tokens = b_summary.get("tokens", {})
323
+ lines.append(f"| Tokens | {a_tokens.get('mean', 0):.0f} ± {a_tokens.get('stddev', 0):.0f} | {b_tokens.get('mean', 0):.0f} ± {b_tokens.get('stddev', 0):.0f} | {delta.get('tokens', '—')} |")
324
+
325
+ # Notes section
326
+ if benchmark.get("notes"):
327
+ lines.extend([
328
+ "",
329
+ "## Notes",
330
+ ""
331
+ ])
332
+ for note in benchmark["notes"]:
333
+ lines.append(f"- {note}")
334
+
335
+ return "\n".join(lines)
336
+
337
+
338
+ def main():
339
+ parser = argparse.ArgumentParser(
340
+ description="Aggregate benchmark run results into summary statistics"
341
+ )
342
+ parser.add_argument(
343
+ "benchmark_dir",
344
+ type=Path,
345
+ help="Path to the benchmark directory"
346
+ )
347
+ parser.add_argument(
348
+ "--skill-name",
349
+ default="",
350
+ help="Name of the skill being benchmarked"
351
+ )
352
+ parser.add_argument(
353
+ "--skill-path",
354
+ default="",
355
+ help="Path to the skill being benchmarked"
356
+ )
357
+ parser.add_argument(
358
+ "--output", "-o",
359
+ type=Path,
360
+ help="Output path for benchmark.json (default: <benchmark_dir>/benchmark.json)"
361
+ )
362
+
363
+ args = parser.parse_args()
364
+
365
+ if not args.benchmark_dir.exists():
366
+ print(f"Directory not found: {args.benchmark_dir}")
367
+ sys.exit(1)
368
+
369
+ # Generate benchmark
370
+ benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path)
371
+
372
+ # Determine output paths
373
+ output_json = args.output or (args.benchmark_dir / "benchmark.json")
374
+ output_md = output_json.with_suffix(".md")
375
+
376
+ # Write benchmark.json
377
+ with open(output_json, "w") as f:
378
+ json.dump(benchmark, f, indent=2)
379
+ print(f"Generated: {output_json}")
380
+
381
+ # Write benchmark.md
382
+ markdown = generate_markdown(benchmark)
383
+ with open(output_md, "w") as f:
384
+ f.write(markdown)
385
+ print(f"Generated: {output_md}")
386
+
387
+ # Print summary
388
+ run_summary = benchmark["run_summary"]
389
+ configs = [k for k in run_summary if k != "delta"]
390
+ delta = run_summary.get("delta", {})
391
+
392
+ print(f"\nSummary:")
393
+ for config in configs:
394
+ pr = run_summary[config]["pass_rate"]["mean"]
395
+ label = config.replace("_", " ").title()
396
+ print(f" {label}: {pr*100:.1f}% pass rate")
397
+ print(f" Delta: {delta.get('pass_rate', '—')}")
398
+
399
+
400
+ if __name__ == "__main__":
401
+ main()