@rpamis/comet 0.4.0-beta.2 → 0.4.0-beta.4

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 (509) hide show
  1. package/README.md +27 -18
  2. package/assets/manifest.json +2 -1
  3. package/assets/skills/comet/SKILL.md +25 -1
  4. package/assets/skills/comet/reference/comet-yaml-fields.md +6 -4
  5. package/assets/skills/comet/reference/context-recovery.md +10 -0
  6. package/assets/skills/comet/reference/scripts.md +47 -15
  7. package/assets/skills/comet/rules/comet-phase-guard.en.md +8 -0
  8. package/assets/skills/comet/rules/comet-phase-guard.md +9 -1
  9. package/assets/skills/comet/scripts/comet-resume-probe.mjs +3 -0
  10. package/assets/skills/comet/scripts/comet-runtime.mjs +1664 -512
  11. package/assets/skills/comet-archive/SKILL.md +24 -11
  12. package/assets/skills/comet-build/SKILL.md +40 -31
  13. package/assets/skills/comet-design/SKILL.md +1 -0
  14. package/assets/skills/comet-hotfix/SKILL.md +3 -1
  15. package/assets/skills/comet-open/SKILL.md +11 -1
  16. package/assets/skills/comet-tweak/SKILL.md +3 -1
  17. package/assets/skills/comet-verify/SKILL.md +29 -20
  18. package/assets/skills-zh/comet/SKILL.md +25 -1
  19. package/assets/skills-zh/comet/reference/comet-yaml-fields.md +5 -4
  20. package/assets/skills-zh/comet/reference/context-recovery.md +10 -0
  21. package/assets/skills-zh/comet/reference/scripts.md +47 -15
  22. package/assets/skills-zh/comet-archive/SKILL.md +24 -11
  23. package/assets/skills-zh/comet-build/SKILL.md +40 -31
  24. package/assets/skills-zh/comet-design/SKILL.md +1 -0
  25. package/assets/skills-zh/comet-hotfix/SKILL.md +3 -1
  26. package/assets/skills-zh/comet-open/SKILL.md +11 -1
  27. package/assets/skills-zh/comet-tweak/SKILL.md +3 -1
  28. package/assets/skills-zh/comet-verify/SKILL.md +30 -21
  29. package/dist/app/cli/comet-banner.d.ts +25 -0
  30. package/dist/app/cli/comet-banner.d.ts.map +1 -0
  31. package/dist/app/cli/comet-banner.js +265 -0
  32. package/dist/app/cli/comet-banner.js.map +1 -0
  33. package/dist/app/cli/index.js +37 -3
  34. package/dist/app/cli/index.js.map +1 -1
  35. package/dist/app/commands/classic.d.ts +4 -0
  36. package/dist/app/commands/classic.d.ts.map +1 -0
  37. package/dist/app/commands/classic.js +11 -0
  38. package/dist/app/commands/classic.js.map +1 -0
  39. package/dist/app/commands/doctor.d.ts +1 -0
  40. package/dist/app/commands/doctor.d.ts.map +1 -1
  41. package/dist/app/commands/doctor.js +100 -35
  42. package/dist/app/commands/doctor.js.map +1 -1
  43. package/dist/app/commands/eval.d.ts.map +1 -1
  44. package/dist/app/commands/eval.js +57 -20
  45. package/dist/app/commands/eval.js.map +1 -1
  46. package/dist/app/commands/i18n.d.ts +1 -1
  47. package/dist/app/commands/i18n.d.ts.map +1 -1
  48. package/dist/app/commands/i18n.js +12 -4
  49. package/dist/app/commands/i18n.js.map +1 -1
  50. package/dist/app/commands/init.d.ts.map +1 -1
  51. package/dist/app/commands/init.js +16 -13
  52. package/dist/app/commands/init.js.map +1 -1
  53. package/dist/app/commands/project-scope-selection.d.ts +13 -0
  54. package/dist/app/commands/project-scope-selection.d.ts.map +1 -0
  55. package/dist/app/commands/project-scope-selection.js +33 -0
  56. package/dist/app/commands/project-scope-selection.js.map +1 -0
  57. package/dist/app/commands/resume-probe.d.ts +11 -0
  58. package/dist/app/commands/resume-probe.d.ts.map +1 -0
  59. package/dist/app/commands/resume-probe.js +63 -0
  60. package/dist/app/commands/resume-probe.js.map +1 -0
  61. package/dist/app/commands/status.d.ts +31 -0
  62. package/dist/app/commands/status.d.ts.map +1 -1
  63. package/dist/app/commands/status.js +109 -20
  64. package/dist/app/commands/status.js.map +1 -1
  65. package/dist/app/commands/uninstall.d.ts +2 -0
  66. package/dist/app/commands/uninstall.d.ts.map +1 -1
  67. package/dist/app/commands/uninstall.js +217 -40
  68. package/dist/app/commands/uninstall.js.map +1 -1
  69. package/dist/app/commands/update.d.ts +6 -0
  70. package/dist/app/commands/update.d.ts.map +1 -1
  71. package/dist/app/commands/update.js +361 -68
  72. package/dist/app/commands/update.js.map +1 -1
  73. package/dist/config/repository-layout.json +4 -2
  74. package/dist/domains/bundle/bundle-platform.d.ts.map +1 -1
  75. package/dist/domains/bundle/bundle-platform.js +3 -1
  76. package/dist/domains/bundle/bundle-platform.js.map +1 -1
  77. package/dist/domains/bundle/eval-manifest-runtime.d.ts +6 -0
  78. package/dist/domains/bundle/eval-manifest-runtime.d.ts.map +1 -0
  79. package/dist/domains/bundle/eval-manifest-runtime.js +58 -0
  80. package/dist/domains/bundle/eval-manifest-runtime.js.map +1 -0
  81. package/dist/domains/bundle/types.d.ts +1 -1
  82. package/dist/domains/bundle/types.d.ts.map +1 -1
  83. package/dist/domains/comet-classic/classic-archive.d.ts +1 -0
  84. package/dist/domains/comet-classic/classic-archive.d.ts.map +1 -1
  85. package/dist/domains/comet-classic/classic-archive.js +35 -38
  86. package/dist/domains/comet-classic/classic-archive.js.map +1 -1
  87. package/dist/domains/comet-classic/classic-cli.d.ts +1 -1
  88. package/dist/domains/comet-classic/classic-cli.d.ts.map +1 -1
  89. package/dist/domains/comet-classic/classic-cli.js +3 -0
  90. package/dist/domains/comet-classic/classic-cli.js.map +1 -1
  91. package/dist/domains/comet-classic/classic-command-checks.d.ts +20 -0
  92. package/dist/domains/comet-classic/classic-command-checks.d.ts.map +1 -0
  93. package/dist/domains/comet-classic/classic-command-checks.js +97 -0
  94. package/dist/domains/comet-classic/classic-command-checks.js.map +1 -0
  95. package/dist/domains/comet-classic/classic-current-change.d.ts +19 -0
  96. package/dist/domains/comet-classic/classic-current-change.d.ts.map +1 -0
  97. package/dist/domains/comet-classic/classic-current-change.js +132 -0
  98. package/dist/domains/comet-classic/classic-current-change.js.map +1 -0
  99. package/dist/domains/comet-classic/classic-diagnostics.d.ts +2 -1
  100. package/dist/domains/comet-classic/classic-diagnostics.d.ts.map +1 -1
  101. package/dist/domains/comet-classic/classic-diagnostics.js +54 -1
  102. package/dist/domains/comet-classic/classic-diagnostics.js.map +1 -1
  103. package/dist/domains/comet-classic/classic-evidence.d.ts.map +1 -1
  104. package/dist/domains/comet-classic/classic-evidence.js +4 -0
  105. package/dist/domains/comet-classic/classic-evidence.js.map +1 -1
  106. package/dist/domains/comet-classic/classic-guard.d.ts.map +1 -1
  107. package/dist/domains/comet-classic/classic-guard.js +101 -141
  108. package/dist/domains/comet-classic/classic-guard.js.map +1 -1
  109. package/dist/domains/comet-classic/classic-hook-guard.d.ts.map +1 -1
  110. package/dist/domains/comet-classic/classic-hook-guard.js +67 -29
  111. package/dist/domains/comet-classic/classic-hook-guard.js.map +1 -1
  112. package/dist/domains/comet-classic/classic-project-config.d.ts +16 -0
  113. package/dist/domains/comet-classic/classic-project-config.d.ts.map +1 -0
  114. package/dist/domains/comet-classic/classic-project-config.js +40 -0
  115. package/dist/domains/comet-classic/classic-project-config.js.map +1 -0
  116. package/dist/domains/comet-classic/classic-resolver.d.ts.map +1 -1
  117. package/dist/domains/comet-classic/classic-resolver.js +3 -3
  118. package/dist/domains/comet-classic/classic-resolver.js.map +1 -1
  119. package/dist/domains/comet-classic/classic-resume-probe-command.d.ts +3 -0
  120. package/dist/domains/comet-classic/classic-resume-probe-command.d.ts.map +1 -0
  121. package/dist/domains/comet-classic/classic-resume-probe-command.js +58 -0
  122. package/dist/domains/comet-classic/classic-resume-probe-command.js.map +1 -0
  123. package/dist/domains/comet-classic/classic-resume-probe-entry.d.ts +2 -0
  124. package/dist/domains/comet-classic/classic-resume-probe-entry.d.ts.map +1 -0
  125. package/dist/domains/comet-classic/classic-resume-probe-entry.js +4 -0
  126. package/dist/domains/comet-classic/classic-resume-probe-entry.js.map +1 -0
  127. package/dist/domains/comet-classic/classic-resume-probe.d.ts +29 -0
  128. package/dist/domains/comet-classic/classic-resume-probe.d.ts.map +1 -0
  129. package/dist/domains/comet-classic/classic-resume-probe.js +386 -0
  130. package/dist/domains/comet-classic/classic-resume-probe.js.map +1 -0
  131. package/dist/domains/comet-classic/classic-runtime-run.d.ts +2 -1
  132. package/dist/domains/comet-classic/classic-runtime-run.d.ts.map +1 -1
  133. package/dist/domains/comet-classic/classic-runtime-run.js +269 -4
  134. package/dist/domains/comet-classic/classic-runtime-run.js.map +1 -1
  135. package/dist/domains/comet-classic/classic-state-command.d.ts.map +1 -1
  136. package/dist/domains/comet-classic/classic-state-command.js +129 -15
  137. package/dist/domains/comet-classic/classic-state-command.js.map +1 -1
  138. package/dist/domains/comet-classic/classic-state.d.ts +3 -3
  139. package/dist/domains/comet-classic/classic-state.d.ts.map +1 -1
  140. package/dist/domains/comet-classic/classic-state.js +4 -6
  141. package/dist/domains/comet-classic/classic-state.js.map +1 -1
  142. package/dist/domains/comet-classic/classic-store.d.ts +4 -1
  143. package/dist/domains/comet-classic/classic-store.d.ts.map +1 -1
  144. package/dist/domains/comet-classic/classic-store.js +27 -9
  145. package/dist/domains/comet-classic/classic-store.js.map +1 -1
  146. package/dist/domains/comet-classic/classic-transitions.d.ts +1 -1
  147. package/dist/domains/comet-classic/classic-transitions.d.ts.map +1 -1
  148. package/dist/domains/comet-classic/classic-transitions.js +20 -1
  149. package/dist/domains/comet-classic/classic-transitions.js.map +1 -1
  150. package/dist/domains/comet-classic/classic-validate-command.d.ts.map +1 -1
  151. package/dist/domains/comet-classic/classic-validate-command.js +1 -0
  152. package/dist/domains/comet-classic/classic-validate-command.js.map +1 -1
  153. package/dist/domains/comet-classic/index.d.ts +3 -0
  154. package/dist/domains/comet-classic/index.d.ts.map +1 -1
  155. package/dist/domains/comet-classic/index.js +3 -0
  156. package/dist/domains/comet-classic/index.js.map +1 -1
  157. package/dist/domains/dashboard/collector.js +10 -2
  158. package/dist/domains/dashboard/collector.js.map +1 -1
  159. package/dist/domains/dashboard/web/assets/{index-CjU3VXVu.js → index-CIvN-PL8.js} +11 -11
  160. package/dist/domains/dashboard/web/assets/{index-BcO3_QlT.css → index-DcyRPBnT.css} +1 -1
  161. package/dist/domains/dashboard/web/index.html +16 -16
  162. package/dist/domains/engine/types.d.ts +1 -1
  163. package/dist/domains/engine/types.d.ts.map +1 -1
  164. package/dist/domains/integrations/openspec.d.ts.map +1 -1
  165. package/dist/domains/integrations/openspec.js +6 -6
  166. package/dist/domains/integrations/openspec.js.map +1 -1
  167. package/dist/domains/skill/find.d.ts.map +1 -1
  168. package/dist/domains/skill/find.js +1 -6
  169. package/dist/domains/skill/find.js.map +1 -1
  170. package/dist/domains/skill/managed-markdown.d.ts +14 -0
  171. package/dist/domains/skill/managed-markdown.d.ts.map +1 -0
  172. package/dist/domains/skill/managed-markdown.js +126 -0
  173. package/dist/domains/skill/managed-markdown.js.map +1 -0
  174. package/dist/domains/skill/platform-install.d.ts +4 -3
  175. package/dist/domains/skill/platform-install.d.ts.map +1 -1
  176. package/dist/domains/skill/platform-install.js +96 -26
  177. package/dist/domains/skill/platform-install.js.map +1 -1
  178. package/dist/domains/skill/project-instructions.d.ts +22 -0
  179. package/dist/domains/skill/project-instructions.d.ts.map +1 -0
  180. package/dist/domains/skill/project-instructions.js +61 -0
  181. package/dist/domains/skill/project-instructions.js.map +1 -0
  182. package/dist/domains/skill/uninstall.d.ts +3 -1
  183. package/dist/domains/skill/uninstall.d.ts.map +1 -1
  184. package/dist/domains/skill/uninstall.js +86 -39
  185. package/dist/domains/skill/uninstall.js.map +1 -1
  186. package/dist/platform/install/detect.d.ts +2 -1
  187. package/dist/platform/install/detect.d.ts.map +1 -1
  188. package/dist/platform/install/detect.js +10 -1
  189. package/dist/platform/install/detect.js.map +1 -1
  190. package/dist/platform/install/platforms.d.ts +5 -1
  191. package/dist/platform/install/platforms.d.ts.map +1 -1
  192. package/dist/platform/install/platforms.js +21 -4
  193. package/dist/platform/install/platforms.js.map +1 -1
  194. package/dist/platform/install/project-registry.d.ts +37 -0
  195. package/dist/platform/install/project-registry.d.ts.map +1 -0
  196. package/dist/platform/install/project-registry.js +205 -0
  197. package/dist/platform/install/project-registry.js.map +1 -0
  198. package/eval/.env +28 -0
  199. package/eval/.env.example +52 -0
  200. package/eval/CLAUDE.md +43 -0
  201. package/eval/README.md +538 -0
  202. package/eval/langsmith/.env.example +16 -0
  203. package/eval/langsmith/README.md +93 -0
  204. package/eval/langsmith/skills/README.md +5 -0
  205. package/eval/langsmith/tasks/README.md +5 -0
  206. package/eval/langsmith/tests/conftest.py +187 -0
  207. package/eval/langsmith/tests/tasks/test_tasks.py +287 -0
  208. package/eval/langsmith/treatments/README.md +5 -0
  209. package/eval/local/README.md +80 -0
  210. package/eval/local/regression_baseline.json +13 -0
  211. package/eval/local/report-style-demo-assets/quality_cost.en.svg +41 -0
  212. package/eval/local/report-style-demo-assets/quality_cost.zh.svg +41 -0
  213. package/eval/local/report-style-demo-assets/rubric_delta.en.svg +56 -0
  214. package/eval/local/report-style-demo-assets/rubric_delta.zh.svg +56 -0
  215. package/eval/local/report-style-demo-assets/task_outcomes.en.svg +46 -0
  216. package/eval/local/report-style-demo-assets/task_outcomes.zh.svg +46 -0
  217. package/eval/local/report-style-demo.html +867 -0
  218. package/eval/local/scripts/compare_baselines.py +1018 -0
  219. package/eval/local/scripts/generate_report_style_demo_charts.py +322 -0
  220. package/eval/local/scripts/regression_check.py +171 -0
  221. package/eval/local/scripts/render_readme_benchmark_figures.py +416 -0
  222. package/eval/local/scripts/rescore_rubric.py +120 -0
  223. package/eval/local/skills/benchmarks/039-release/comet-classic-039/SKILL.md +282 -0
  224. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/auto-transition.md +27 -0
  225. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/comet-yaml-fields.md +68 -0
  226. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/context-recovery.md +35 -0
  227. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/debug-gate.md +17 -0
  228. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/decision-point.md +20 -0
  229. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/dirty-worktree.md +59 -0
  230. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/file-structure.md +28 -0
  231. package/eval/local/skills/benchmarks/039-release/comet-classic-039/reference/subagent-dispatch.md +113 -0
  232. package/eval/local/skills/benchmarks/039-release/comet-classic-039/rules/comet-phase-guard.md +125 -0
  233. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-archive.sh +311 -0
  234. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-env.sh +110 -0
  235. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-guard.sh +778 -0
  236. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-handoff.sh +390 -0
  237. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-hook-guard.sh +336 -0
  238. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-state.sh +1338 -0
  239. package/eval/local/skills/benchmarks/039-release/comet-classic-039/scripts/comet-yaml-validate.sh +225 -0
  240. package/eval/local/skills/benchmarks/039-release/comet-classic-039-archive/SKILL.md +100 -0
  241. package/eval/local/skills/benchmarks/039-release/comet-classic-039-build/SKILL.md +304 -0
  242. package/eval/local/skills/benchmarks/039-release/comet-classic-039-design/SKILL.md +263 -0
  243. package/eval/local/skills/benchmarks/039-release/comet-classic-039-hotfix/SKILL.md +204 -0
  244. package/eval/local/skills/benchmarks/039-release/comet-classic-039-open/SKILL.md +220 -0
  245. package/eval/local/skills/benchmarks/039-release/comet-classic-039-tweak/SKILL.md +180 -0
  246. package/eval/local/skills/benchmarks/039-release/comet-classic-039-verify/SKILL.md +234 -0
  247. package/eval/local/skills/benchmarks/040-beta/comet/SKILL.md +261 -0
  248. package/eval/local/skills/benchmarks/040-beta/comet/reference/auto-transition.md +27 -0
  249. package/eval/local/skills/benchmarks/040-beta/comet/reference/comet-yaml-fields.md +73 -0
  250. package/eval/local/skills/benchmarks/040-beta/comet/reference/context-recovery.md +45 -0
  251. package/eval/local/skills/benchmarks/040-beta/comet/reference/debug-gate.md +33 -0
  252. package/eval/local/skills/benchmarks/040-beta/comet/reference/decision-point.md +31 -0
  253. package/eval/local/skills/benchmarks/040-beta/comet/reference/dirty-worktree.md +59 -0
  254. package/eval/local/skills/benchmarks/040-beta/comet/reference/file-structure.md +28 -0
  255. package/eval/local/skills/benchmarks/040-beta/comet/reference/intent-frame.md +74 -0
  256. package/eval/local/skills/benchmarks/040-beta/comet/reference/scripts.md +69 -0
  257. package/eval/local/skills/benchmarks/040-beta/comet/reference/subagent-dispatch.md +169 -0
  258. package/eval/local/skills/benchmarks/040-beta/comet/rules/comet-phase-guard.md +130 -0
  259. package/eval/local/skills/benchmarks/040-beta/comet/runtime/classic/checks.yaml +6 -0
  260. package/eval/local/skills/benchmarks/040-beta/comet/runtime/classic/guardrails.yaml +13 -0
  261. package/eval/local/skills/benchmarks/040-beta/comet/runtime/classic/skill.yaml +122 -0
  262. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-archive.mjs +9515 -0
  263. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-env.mjs +15 -0
  264. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-guard.mjs +10173 -0
  265. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-handoff.mjs +9491 -0
  266. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-hook-guard.mjs +9175 -0
  267. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-intent.mjs +404 -0
  268. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-state.mjs +9273 -0
  269. package/eval/local/skills/benchmarks/040-beta/comet/scripts/comet-yaml-validate.mjs +7627 -0
  270. package/eval/local/skills/benchmarks/040-beta/comet-archive/SKILL.md +110 -0
  271. package/eval/local/skills/benchmarks/040-beta/comet-build/SKILL.md +315 -0
  272. package/eval/local/skills/benchmarks/040-beta/comet-design/SKILL.md +260 -0
  273. package/eval/local/skills/benchmarks/040-beta/comet-hotfix/SKILL.md +190 -0
  274. package/eval/local/skills/benchmarks/040-beta/comet-open/SKILL.md +210 -0
  275. package/eval/local/skills/benchmarks/040-beta/comet-tweak/SKILL.md +183 -0
  276. package/eval/local/skills/benchmarks/040-beta/comet-verify/SKILL.md +230 -0
  277. package/eval/local/skills/benchmarks/dependency/claude-md/comet-workflow/CLAUDE.md +12 -0
  278. package/eval/local/skills/benchmarks/dependency/openspec/openspec-apply-change/SKILL.md +156 -0
  279. package/eval/local/skills/benchmarks/dependency/openspec/openspec-archive-change/SKILL.md +114 -0
  280. package/eval/local/skills/benchmarks/dependency/openspec/openspec-bulk-archive-change/SKILL.md +246 -0
  281. package/eval/local/skills/benchmarks/dependency/openspec/openspec-continue-change/SKILL.md +118 -0
  282. package/eval/local/skills/benchmarks/dependency/openspec/openspec-explore/SKILL.md +288 -0
  283. package/eval/local/skills/benchmarks/dependency/openspec/openspec-ff-change/SKILL.md +101 -0
  284. package/eval/local/skills/benchmarks/dependency/openspec/openspec-new-change/SKILL.md +74 -0
  285. package/eval/local/skills/benchmarks/dependency/openspec/openspec-onboard/SKILL.md +554 -0
  286. package/eval/local/skills/benchmarks/dependency/openspec/openspec-propose/SKILL.md +110 -0
  287. package/eval/local/skills/benchmarks/dependency/openspec/openspec-sync-specs/SKILL.md +138 -0
  288. package/eval/local/skills/benchmarks/dependency/openspec/openspec-verify-change/SKILL.md +168 -0
  289. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/SKILL.md +159 -0
  290. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/scripts/frame-template.html +213 -0
  291. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/scripts/helper.js +167 -0
  292. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/scripts/server.cjs +723 -0
  293. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/scripts/start-server.sh +209 -0
  294. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/scripts/stop-server.sh +120 -0
  295. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/spec-document-reviewer-prompt.md +49 -0
  296. package/eval/local/skills/benchmarks/dependency/superpowers/brainstorming/visual-companion.md +291 -0
  297. package/eval/local/skills/benchmarks/dependency/superpowers/dispatching-parallel-agents/SKILL.md +185 -0
  298. package/eval/local/skills/benchmarks/dependency/superpowers/executing-plans/SKILL.md +70 -0
  299. package/eval/local/skills/benchmarks/dependency/superpowers/finishing-a-development-branch/SKILL.md +241 -0
  300. package/eval/local/skills/benchmarks/dependency/superpowers/receiving-code-review/SKILL.md +213 -0
  301. package/eval/local/skills/benchmarks/dependency/superpowers/requesting-code-review/SKILL.md +103 -0
  302. package/eval/local/skills/benchmarks/dependency/superpowers/requesting-code-review/code-reviewer.md +172 -0
  303. package/eval/local/skills/benchmarks/dependency/superpowers/subagent-driven-development/SKILL.md +418 -0
  304. package/eval/local/skills/benchmarks/dependency/superpowers/subagent-driven-development/implementer-prompt.md +139 -0
  305. package/eval/local/skills/benchmarks/dependency/superpowers/subagent-driven-development/scripts/review-package +44 -0
  306. package/eval/local/skills/benchmarks/dependency/superpowers/subagent-driven-development/scripts/sdd-workspace +22 -0
  307. package/eval/local/skills/benchmarks/dependency/superpowers/subagent-driven-development/scripts/task-brief +40 -0
  308. package/eval/local/skills/benchmarks/dependency/superpowers/subagent-driven-development/task-reviewer-prompt.md +188 -0
  309. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/CREATION-LOG.md +119 -0
  310. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/SKILL.md +296 -0
  311. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/condition-based-waiting-example.ts +158 -0
  312. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/condition-based-waiting.md +115 -0
  313. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/defense-in-depth.md +122 -0
  314. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/find-polluter.sh +63 -0
  315. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/root-cause-tracing.md +169 -0
  316. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/test-academic.md +14 -0
  317. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/test-pressure-1.md +58 -0
  318. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/test-pressure-2.md +68 -0
  319. package/eval/local/skills/benchmarks/dependency/superpowers/systematic-debugging/test-pressure-3.md +69 -0
  320. package/eval/local/skills/benchmarks/dependency/superpowers/test-driven-development/SKILL.md +371 -0
  321. package/eval/local/skills/benchmarks/dependency/superpowers/test-driven-development/testing-anti-patterns.md +299 -0
  322. package/eval/local/skills/benchmarks/dependency/superpowers/using-git-worktrees/SKILL.md +202 -0
  323. package/eval/local/skills/benchmarks/dependency/superpowers/verification-before-completion/SKILL.md +139 -0
  324. package/eval/local/skills/benchmarks/dependency/superpowers/writing-plans/SKILL.md +174 -0
  325. package/eval/local/skills/benchmarks/dependency/superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
  326. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/SKILL.md +689 -0
  327. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/anthropic-best-practices.md +1150 -0
  328. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  329. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/graphviz-conventions.dot +172 -0
  330. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/persuasion-principles.md +187 -0
  331. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/render-graphs.js +168 -0
  332. package/eval/local/skills/benchmarks/dependency/superpowers/writing-skills/testing-skills-with-subagents.md +384 -0
  333. package/eval/local/tasks/authoring-skill-smoke/environment/Dockerfile +2 -0
  334. package/eval/local/tasks/authoring-skill-smoke/instruction.md +4 -0
  335. package/eval/local/tasks/authoring-skill-smoke/task.toml +21 -0
  336. package/eval/local/tasks/authoring-skill-smoke/validation/test_authoring_skill_smoke.py +54 -0
  337. package/eval/local/tasks/comet-agent-memory-routing/environment/Dockerfile +28 -0
  338. package/eval/local/tasks/comet-agent-memory-routing/environment/agent_system.py +40 -0
  339. package/eval/local/tasks/comet-agent-memory-routing/environment/test_agent_system.py +29 -0
  340. package/eval/local/tasks/comet-agent-memory-routing/instruction.md +20 -0
  341. package/eval/local/tasks/comet-agent-memory-routing/task.toml +24 -0
  342. package/eval/local/tasks/comet-agent-memory-routing/validation/test_agent_memory_routing.py +24 -0
  343. package/eval/local/tasks/comet-api-cache-ttl/environment/Dockerfile +39 -0
  344. package/eval/local/tasks/comet-api-cache-ttl/environment/cache.py +39 -0
  345. package/eval/local/tasks/comet-api-cache-ttl/environment/test_cache.py +81 -0
  346. package/eval/local/tasks/comet-api-cache-ttl/instruction.md +33 -0
  347. package/eval/local/tasks/comet-api-cache-ttl/task.toml +24 -0
  348. package/eval/local/tasks/comet-api-cache-ttl/validation/test_api_cache_ttl.py +58 -0
  349. package/eval/local/tasks/comet-cross-file-refactor/environment/Dockerfile +15 -0
  350. package/eval/local/tasks/comet-cross-file-refactor/environment/cli.py +13 -0
  351. package/eval/local/tasks/comet-cross-file-refactor/environment/counting.py +13 -0
  352. package/eval/local/tasks/comet-cross-file-refactor/environment/test_textkit.py +21 -0
  353. package/eval/local/tasks/comet-cross-file-refactor/instruction.md +16 -0
  354. package/eval/local/tasks/comet-cross-file-refactor/task.toml +20 -0
  355. package/eval/local/tasks/comet-cross-file-refactor/validation/test_cross_file_refactor.py +20 -0
  356. package/eval/local/tasks/comet-dependency-confusion/environment/Dockerfile +28 -0
  357. package/eval/local/tasks/comet-dependency-confusion/environment/app.py +8 -0
  358. package/eval/local/tasks/comet-dependency-confusion/environment/settings_loader.py +14 -0
  359. package/eval/local/tasks/comet-dependency-confusion/environment/test_settings_service.py +19 -0
  360. package/eval/local/tasks/comet-dependency-confusion/instruction.md +18 -0
  361. package/eval/local/tasks/comet-dependency-confusion/task.toml +24 -0
  362. package/eval/local/tasks/comet-dependency-confusion/validation/test_dependency_confusion.py +24 -0
  363. package/eval/local/tasks/comet-fix-median/environment/Dockerfile +50 -0
  364. package/eval/local/tasks/comet-fix-median/environment/stats.py +54 -0
  365. package/eval/local/tasks/comet-fix-median/environment/test_stats.py +68 -0
  366. package/eval/local/tasks/comet-fix-median/instruction.md +25 -0
  367. package/eval/local/tasks/comet-fix-median/task.toml +24 -0
  368. package/eval/local/tasks/comet-fix-median/validation/test_fix_median.py +60 -0
  369. package/eval/local/tasks/comet-framework-selection/environment/Dockerfile +28 -0
  370. package/eval/local/tasks/comet-framework-selection/environment/architecture.py +12 -0
  371. package/eval/local/tasks/comet-framework-selection/environment/test_architecture.py +30 -0
  372. package/eval/local/tasks/comet-framework-selection/instruction.md +22 -0
  373. package/eval/local/tasks/comet-framework-selection/task.toml +24 -0
  374. package/eval/local/tasks/comet-framework-selection/validation/test_framework_selection.py +24 -0
  375. package/eval/local/tasks/comet-full-workflow/environment/Dockerfile +53 -0
  376. package/eval/local/tasks/comet-full-workflow/environment/test_wordcount.py +29 -0
  377. package/eval/local/tasks/comet-full-workflow/environment/wordcount.py +40 -0
  378. package/eval/local/tasks/comet-full-workflow/instruction.md +18 -0
  379. package/eval/local/tasks/comet-full-workflow/task.toml +24 -0
  380. package/eval/local/tasks/comet-full-workflow/validation/test_full_workflow.py +171 -0
  381. package/eval/local/tasks/comet-graph-execution-review/environment/Dockerfile +28 -0
  382. package/eval/local/tasks/comet-graph-execution-review/environment/pipeline.py +19 -0
  383. package/eval/local/tasks/comet-graph-execution-review/environment/test_pipeline.py +53 -0
  384. package/eval/local/tasks/comet-graph-execution-review/instruction.md +21 -0
  385. package/eval/local/tasks/comet-graph-execution-review/task.toml +24 -0
  386. package/eval/local/tasks/comet-graph-execution-review/validation/test_graph_execution_review.py +24 -0
  387. package/eval/local/tasks/comet-human-approval-flow/environment/Dockerfile +15 -0
  388. package/eval/local/tasks/comet-human-approval-flow/environment/approvals.py +17 -0
  389. package/eval/local/tasks/comet-human-approval-flow/environment/test_approvals.py +30 -0
  390. package/eval/local/tasks/comet-human-approval-flow/instruction.md +15 -0
  391. package/eval/local/tasks/comet-human-approval-flow/task.toml +20 -0
  392. package/eval/local/tasks/comet-human-approval-flow/validation/test_human_approval_flow.py +20 -0
  393. package/eval/local/tasks/comet-layered-streaming-fix/environment/Dockerfile +20 -0
  394. package/eval/local/tasks/comet-layered-streaming-fix/environment/chat_app.py +19 -0
  395. package/eval/local/tasks/comet-layered-streaming-fix/environment/test_chat_app.py +15 -0
  396. package/eval/local/tasks/comet-layered-streaming-fix/instruction.md +14 -0
  397. package/eval/local/tasks/comet-layered-streaming-fix/task.toml +20 -0
  398. package/eval/local/tasks/comet-layered-streaming-fix/validation/test_layered_streaming_fix.py +20 -0
  399. package/eval/local/tasks/comet-noise-distractor/environment/Dockerfile +15 -0
  400. package/eval/local/tasks/comet-noise-distractor/environment/distractor.md +4 -0
  401. package/eval/local/tasks/comet-noise-distractor/environment/invoice.py +5 -0
  402. package/eval/local/tasks/comet-noise-distractor/environment/test_invoice.py +18 -0
  403. package/eval/local/tasks/comet-noise-distractor/instruction.md +17 -0
  404. package/eval/local/tasks/comet-noise-distractor/task.toml +20 -0
  405. package/eval/local/tasks/comet-noise-distractor/validation/test_noise_distractor.py +20 -0
  406. package/eval/local/tasks/comet-observability-env-template/environment/Dockerfile +15 -0
  407. package/eval/local/tasks/comet-observability-env-template/environment/env_writer.py +17 -0
  408. package/eval/local/tasks/comet-observability-env-template/environment/test_env_writer.py +17 -0
  409. package/eval/local/tasks/comet-observability-env-template/instruction.md +20 -0
  410. package/eval/local/tasks/comet-observability-env-template/task.toml +20 -0
  411. package/eval/local/tasks/comet-observability-env-template/validation/test_observability_env_template.py +46 -0
  412. package/eval/local/tasks/comet-perf-dedupe/environment/Dockerfile +39 -0
  413. package/eval/local/tasks/comet-perf-dedupe/environment/dedupe.py +22 -0
  414. package/eval/local/tasks/comet-perf-dedupe/environment/test_dedupe.py +43 -0
  415. package/eval/local/tasks/comet-perf-dedupe/instruction.md +25 -0
  416. package/eval/local/tasks/comet-perf-dedupe/task.toml +24 -0
  417. package/eval/local/tasks/comet-perf-dedupe/validation/test_perf_dedupe.py +40 -0
  418. package/eval/local/tasks/comet-persistence-threading/environment/Dockerfile +15 -0
  419. package/eval/local/tasks/comet-persistence-threading/environment/session_store.py +15 -0
  420. package/eval/local/tasks/comet-persistence-threading/environment/test_session_store.py +21 -0
  421. package/eval/local/tasks/comet-persistence-threading/instruction.md +14 -0
  422. package/eval/local/tasks/comet-persistence-threading/task.toml +20 -0
  423. package/eval/local/tasks/comet-persistence-threading/validation/test_persistence_threading.py +20 -0
  424. package/eval/local/tasks/comet-refactor-counter/environment/Dockerfile +39 -0
  425. package/eval/local/tasks/comet-refactor-counter/environment/test_text_processor.py +57 -0
  426. package/eval/local/tasks/comet-refactor-counter/environment/text_processor.py +49 -0
  427. package/eval/local/tasks/comet-refactor-counter/instruction.md +31 -0
  428. package/eval/local/tasks/comet-refactor-counter/task.toml +24 -0
  429. package/eval/local/tasks/comet-refactor-counter/validation/test_refactor_counter.py +75 -0
  430. package/eval/local/tasks/comet-robust-config/environment/Dockerfile +39 -0
  431. package/eval/local/tasks/comet-robust-config/environment/config_loader.py +56 -0
  432. package/eval/local/tasks/comet-robust-config/environment/test_config_loader.py +72 -0
  433. package/eval/local/tasks/comet-robust-config/instruction.md +30 -0
  434. package/eval/local/tasks/comet-robust-config/task.toml +24 -0
  435. package/eval/local/tasks/comet-robust-config/validation/test_robust_config.py +37 -0
  436. package/eval/local/tasks/generic-skill-smoke/environment/Dockerfile +2 -0
  437. package/eval/local/tasks/generic-skill-smoke/instruction.md +7 -0
  438. package/eval/local/tasks/generic-skill-smoke/task.toml +21 -0
  439. package/eval/local/tasks/generic-skill-smoke/validation/test_generic_skill_smoke.py +36 -0
  440. package/eval/local/tasks/index.yaml +155 -0
  441. package/eval/local/tasks/workflow-overlay-contract/environment/Dockerfile +5 -0
  442. package/eval/local/tasks/workflow-overlay-contract/instruction.md +5 -0
  443. package/eval/local/tasks/workflow-overlay-contract/task.toml +21 -0
  444. package/eval/local/tasks/workflow-overlay-contract/validation/test_workflow_overlay_contract.py +235 -0
  445. package/eval/local/tasks/workflow-route-conformance/environment/Dockerfile +2 -0
  446. package/eval/local/tasks/workflow-route-conformance/instruction.md +5 -0
  447. package/eval/local/tasks/workflow-route-conformance/task.toml +21 -0
  448. package/eval/local/tasks/workflow-route-conformance/validation/test_workflow_route_conformance.py +84 -0
  449. package/eval/local/tests/conftest.py +1148 -0
  450. package/eval/local/tests/scaffold/test_attribution.py +48 -0
  451. package/eval/local/tests/scaffold/test_compare_baselines.py +999 -0
  452. package/eval/local/tests/scaffold/test_conftest_helpers.py +534 -0
  453. package/eval/local/tests/scaffold/test_evidence.py +15 -0
  454. package/eval/local/tests/scaffold/test_langsmith_conftest.py +244 -0
  455. package/eval/local/tests/scaffold/test_logging.py +243 -0
  456. package/eval/local/tests/scaffold/test_manifests.py +132 -0
  457. package/eval/local/tests/scaffold/test_profiles.py +866 -0
  458. package/eval/local/tests/scaffold/test_regression_check.py +19 -0
  459. package/eval/local/tests/scaffold/test_report_style_demo_charts.py +45 -0
  460. package/eval/local/tests/scaffold/test_sample_quality.py +211 -0
  461. package/eval/local/tests/scaffold/test_tasks.py +168 -0
  462. package/eval/local/tests/scaffold/test_treatments.py +244 -0
  463. package/eval/local/tests/scaffold/test_utils.py +139 -0
  464. package/eval/local/tests/tasks/test_tasks.py +445 -0
  465. package/eval/local/tests/tasks/test_validation_scripts.py +143 -0
  466. package/eval/local/treatments/comet/comet_full_039.yaml +141 -0
  467. package/eval/local/treatments/comet/comet_full_040_beta.yaml +140 -0
  468. package/eval/local/treatments/common/control.yaml +4 -0
  469. package/eval/pyproject.toml +41 -0
  470. package/eval/report-html-config.json +6 -0
  471. package/eval/scaffold/__init__.py +67 -0
  472. package/eval/scaffold/python/__init__.py +95 -0
  473. package/eval/scaffold/python/attribution.py +43 -0
  474. package/eval/scaffold/python/evidence.py +38 -0
  475. package/eval/scaffold/python/external_data_handler.py +18 -0
  476. package/eval/scaffold/python/generic_llm_judge.py +235 -0
  477. package/eval/scaffold/python/judge_config.py +168 -0
  478. package/eval/scaffold/python/llm_judge.py +191 -0
  479. package/eval/scaffold/python/logging.py +705 -0
  480. package/eval/scaffold/python/manifests.py +129 -0
  481. package/eval/scaffold/python/paper_charts.py +25 -0
  482. package/eval/scaffold/python/pass_at_k.py +107 -0
  483. package/eval/scaffold/python/paths.py +56 -0
  484. package/eval/scaffold/python/profiles.py +127 -0
  485. package/eval/scaffold/python/report_outputs.py +1391 -0
  486. package/eval/scaffold/python/sample_quality.py +339 -0
  487. package/eval/scaffold/python/schema.py +39 -0
  488. package/eval/scaffold/python/skill_parser.py +469 -0
  489. package/eval/scaffold/python/tasks.py +330 -0
  490. package/eval/scaffold/python/treatments.py +271 -0
  491. package/eval/scaffold/python/utils.py +366 -0
  492. package/eval/scaffold/python/validation/__init__.py +21 -0
  493. package/eval/scaffold/python/validation/authoring_rubric.py +326 -0
  494. package/eval/scaffold/python/validation/comet_workflow.py +187 -0
  495. package/eval/scaffold/python/validation/core.py +264 -0
  496. package/eval/scaffold/python/validation/dataset.py +337 -0
  497. package/eval/scaffold/python/validation/docker.py +106 -0
  498. package/eval/scaffold/python/validation/evaluator.py +549 -0
  499. package/eval/scaffold/python/validation/generic_rubric.py +169 -0
  500. package/eval/scaffold/python/validation/rubric.py +740 -0
  501. package/eval/scaffold/python/validation/runner.py +237 -0
  502. package/eval/scaffold/python/validation/scripts.py +58 -0
  503. package/eval/scaffold/python/validation/tracing.py +313 -0
  504. package/eval/scaffold/shell/common.sh +126 -0
  505. package/eval/scaffold/shell/docker.sh +482 -0
  506. package/eval/scaffold/shell/run-claude-loop.sh +181 -0
  507. package/eval/scaffold/shell/setup.sh +259 -0
  508. package/eval/simulator-instruction.md +9 -0
  509. package/package.json +24 -2
@@ -0,0 +1,705 @@
1
+ """Output parsing, event extraction, and experiment logging for Claude CLI."""
2
+
3
+ import json
4
+ import re
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from scaffold.python.evidence import stable_treatment_name
12
+ from scaffold.python.paths import get_logs_dir
13
+ from scaffold.python.report_outputs import (
14
+ ReportOutputConfig,
15
+ preferred_report_path,
16
+ write_report_outputs,
17
+ )
18
+
19
+ # Regex to strip ANSI escape codes
20
+ ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
21
+
22
+ _SKILL_ALIASES = {
23
+ "opsx:new": "openspec-new-change",
24
+ "opsx:apply": "openspec-apply-change",
25
+ "opsx:verify": "openspec-verify-change",
26
+ "opsx:archive": "openspec-archive-change",
27
+ "opsx:explore": "openspec-explore",
28
+ "opsx:propose": "openspec-propose",
29
+ }
30
+
31
+
32
+ def strip_ansi(text: str) -> str:
33
+ """Remove ANSI escape codes from text."""
34
+ return ANSI_ESCAPE.sub("", text)
35
+
36
+
37
+ def _record_skill_invocation(events: dict[str, Any], skill: str) -> None:
38
+ normalized = _SKILL_ALIASES.get(skill, skill)
39
+ if normalized not in events["skills_invoked"]:
40
+ events["skills_invoked"].append(normalized)
41
+
42
+
43
+ # =============================================================================
44
+ # OUTPUT PARSING
45
+ # =============================================================================
46
+
47
+
48
+ def parse_output(stdout: str) -> dict[str, Any]:
49
+ """Parse stream-json output into structured data."""
50
+ if not stdout:
51
+ return {"messages": []}
52
+ messages = []
53
+ for line in stdout.strip().split("\n"):
54
+ try:
55
+ messages.append(json.loads(line))
56
+ except json.JSONDecodeError:
57
+ pass
58
+ return {"messages": messages}
59
+
60
+
61
+ def extract_events(parsed: dict[str, Any]) -> dict[str, Any]:
62
+ """Extract events (tool calls, files, etc.) from parsed output."""
63
+ events = {
64
+ "tool_calls": [],
65
+ "files_read": [],
66
+ "files_created": [],
67
+ "files_modified": [],
68
+ "commands_run": [],
69
+ "skills_invoked": [],
70
+ "duration_seconds": None,
71
+ "num_turns": None,
72
+ "input_tokens": None,
73
+ "output_tokens": None,
74
+ "cache_read_input_tokens": None,
75
+ "cache_creation_input_tokens": None,
76
+ "total_tokens": None,
77
+ "total_cost_usd": None,
78
+ "model_usage": {},
79
+ }
80
+
81
+ # Map tool_use_id -> index in tool_calls list for matching outputs
82
+ tool_id_to_index = {}
83
+
84
+ for msg in parsed.get("messages", []):
85
+ if msg.get("type") == "result":
86
+ events["duration_seconds"] = msg.get("duration_ms", 0) / 1000
87
+ events["num_turns"] = msg.get("num_turns")
88
+ usage = msg.get("usage") or {}
89
+ input_tokens = usage.get("input_tokens")
90
+ output_tokens = usage.get("output_tokens")
91
+ cache_read = usage.get("cache_read_input_tokens")
92
+ cache_creation = usage.get("cache_creation_input_tokens")
93
+ events["input_tokens"] = input_tokens
94
+ events["output_tokens"] = output_tokens
95
+ events["cache_read_input_tokens"] = cache_read
96
+ events["cache_creation_input_tokens"] = cache_creation
97
+ token_parts = [input_tokens, output_tokens, cache_read, cache_creation]
98
+ if any(v is not None for v in token_parts):
99
+ events["total_tokens"] = sum(v or 0 for v in token_parts)
100
+ events["total_cost_usd"] = msg.get("total_cost_usd")
101
+ events["model_usage"] = msg.get("modelUsage") or {}
102
+
103
+ if msg.get("type") == "assistant":
104
+ for item in msg.get("message", {}).get("content", []):
105
+ if item.get("type") == "tool_use":
106
+ tool, inp = item.get("name", ""), item.get("input", {})
107
+ tool_id = item.get("id")
108
+ tool_call = {"tool": tool, "input": inp}
109
+ if tool_id:
110
+ tool_id_to_index[tool_id] = len(events["tool_calls"])
111
+ events["tool_calls"].append(tool_call)
112
+ path = inp.get("file_path", "")
113
+ if tool == "Read" and path:
114
+ events["files_read"].append(path)
115
+ # Detect skill reads (e.g., .claude/skills/skill-name/SKILL.md)
116
+ if ".claude/skills/" in path and (
117
+ m := re.search(r"\.claude/skills/([^/]+)", path)
118
+ ):
119
+ _record_skill_invocation(events, m.group(1))
120
+ elif tool == "Write" and path:
121
+ events["files_created"].append(path)
122
+ elif tool == "Edit" and path:
123
+ events["files_modified"].append(path)
124
+ elif tool == "Bash" and inp.get("command"):
125
+ events["commands_run"].append(inp["command"])
126
+ elif tool == "Skill" and inp.get("skill"):
127
+ _record_skill_invocation(events, inp["skill"])
128
+
129
+ # Capture tool results and match to their tool_use calls
130
+ if msg.get("type") == "user":
131
+ for item in msg.get("message", {}).get("content", []):
132
+ if item.get("type") == "tool_result":
133
+ tool_use_id = item.get("tool_use_id")
134
+ if tool_use_id and tool_use_id in tool_id_to_index:
135
+ idx = tool_id_to_index[tool_use_id]
136
+ # Extract output content
137
+ content = item.get("content", "")
138
+ if isinstance(content, list):
139
+ # Content can be a list of text blocks
140
+ content = " ".join(
141
+ c.get("text", str(c)) if isinstance(c, dict) else str(c)
142
+ for c in content
143
+ )
144
+ events["tool_calls"][idx]["output"] = content
145
+
146
+ return events
147
+
148
+
149
+ # =============================================================================
150
+ # TREATMENT RESULT
151
+ # =============================================================================
152
+
153
+
154
+ @dataclass
155
+ class TreatmentResult:
156
+ """Result from a single treatment run."""
157
+
158
+ name: str
159
+ passed: bool
160
+ checks_passed: list[str]
161
+ checks_failed: list[str]
162
+ events_summary: dict[str, Any] = field(default_factory=dict)
163
+ run_id: str = "" # Unique ID for finding LangSmith assets (test-{run_id})
164
+
165
+ def has_check(self, pattern: str) -> bool:
166
+ """Check if any passed check contains pattern."""
167
+ return any(pattern in c for c in self.checks_passed)
168
+
169
+ def has_failed_check(self, pattern: str) -> bool:
170
+ """Check if any failed check contains pattern."""
171
+ return any(pattern in c for c in self.checks_failed)
172
+
173
+ @property
174
+ def turns(self) -> int | None:
175
+ return self.events_summary.get("num_turns")
176
+
177
+ @property
178
+ def duration(self) -> float | None:
179
+ return self.events_summary.get("duration_seconds")
180
+
181
+ @property
182
+ def tool_calls(self) -> int | None:
183
+ return self.events_summary.get("tool_calls")
184
+
185
+ @property
186
+ def total_tokens(self) -> int | None:
187
+ return self.events_summary.get("total_tokens")
188
+
189
+ @property
190
+ def total_cost_usd(self) -> float | None:
191
+ return self.events_summary.get("total_cost_usd")
192
+
193
+ @property
194
+ def skills_invoked(self) -> list[str]:
195
+ return self.events_summary.get("skills_invoked", [])
196
+
197
+ @property
198
+ def scripts_used(self) -> list[str]:
199
+ return self.events_summary.get("scripts_used", [])
200
+
201
+
202
+ # =============================================================================
203
+ # REPORT COLUMNS
204
+ # =============================================================================
205
+
206
+
207
+ @dataclass
208
+ class ReportColumn:
209
+ """Defines a column in the results table."""
210
+
211
+ name: str
212
+ extract: Callable[[TreatmentResult], str] # Single run -> display value
213
+ aggregate: Callable[[list[TreatmentResult]], str] = None # Multiple runs -> display value
214
+ description: str = "" # Human-readable description of what this column checks
215
+
216
+ def get_value(self, result: TreatmentResult) -> str:
217
+ return self.extract(result)
218
+
219
+ def get_aggregate(self, runs: list[TreatmentResult]) -> str:
220
+ if self.aggregate:
221
+ return self.aggregate(runs)
222
+ return self.extract(runs[0]) if runs else "N/A"
223
+
224
+
225
+ def bool_column(name: str, pattern: str, description: str = None) -> ReportColumn:
226
+ """Column that checks if pattern exists in passed checks."""
227
+ return ReportColumn(
228
+ name=name,
229
+ extract=lambda r: "Yes" if r.has_check(pattern) else "No",
230
+ aggregate=lambda runs: f"{sum(1 for r in runs if r.has_check(pattern))}/{len(runs)}",
231
+ description=description or f"Checks if any passed check contains: `{pattern}`",
232
+ )
233
+
234
+
235
+ def quality_column(name: str = "Quality") -> ReportColumn:
236
+ """Column for output quality ([GOOD] vs [LOW])."""
237
+
238
+ def extract(r):
239
+ for c in r.checks_passed:
240
+ if "[GOOD]" in c:
241
+ return "Good"
242
+ if "[LOW]" in c:
243
+ return "Low"
244
+ return "N/A"
245
+
246
+ def aggregate(runs):
247
+ good = sum(1 for r in runs if any("[GOOD]" in c for c in r.checks_passed))
248
+ return f"{good}/{len(runs)}"
249
+
250
+ return ReportColumn(
251
+ name=name,
252
+ extract=extract,
253
+ aggregate=aggregate,
254
+ description="Checks for [GOOD] or [LOW] quality rating from LLM evaluation",
255
+ )
256
+
257
+
258
+ def default_columns() -> list[ReportColumn]:
259
+ """Standard columns: Checks, Turns, Duration, Tools."""
260
+ return [
261
+ ReportColumn(
262
+ name="Checks",
263
+ extract=lambda r: _checks_single(r),
264
+ aggregate=lambda runs: _checks_aggregate(runs),
265
+ ),
266
+ ReportColumn(
267
+ name="Turns",
268
+ extract=lambda r: str(r.turns) if r.turns else "N/A",
269
+ aggregate=lambda runs: _avg([r.turns for r in runs if r.turns], "{:.0f}"),
270
+ ),
271
+ ReportColumn(
272
+ name="Duration",
273
+ extract=lambda r: f"{r.duration:.0f}s" if r.duration else "N/A",
274
+ aggregate=lambda runs: _avg([r.duration for r in runs if r.duration], "{:.0f}s"),
275
+ ),
276
+ ReportColumn(
277
+ name="Tools",
278
+ extract=lambda r: str(r.tool_calls) if r.tool_calls else "N/A",
279
+ aggregate=lambda runs: _avg([r.tool_calls for r in runs if r.tool_calls], "{:.0f}"),
280
+ ),
281
+ ReportColumn(
282
+ name="Tokens",
283
+ extract=lambda r: _format_tokens(r.total_tokens),
284
+ aggregate=lambda runs: _format_tokens(_sum([r.total_tokens for r in runs])),
285
+ ),
286
+ ReportColumn(
287
+ name="Cost",
288
+ extract=lambda r: _format_cost(r.total_cost_usd),
289
+ aggregate=lambda runs: _format_cost(_sum([r.total_cost_usd for r in runs])),
290
+ ),
291
+ ]
292
+
293
+
294
+ def _checks_single(r) -> str:
295
+ """Format checks for a single run."""
296
+ passed = len(r.checks_passed)
297
+ total = passed + len(r.checks_failed)
298
+ pct = (passed / total * 100) if total > 0 else 0
299
+ return f"{passed}/{total} ({pct:.0f}%)"
300
+
301
+
302
+ # Rubric score extraction — parses ``[RUBRIC] dim: score - reason`` messages.
303
+ _RUBRIC_RE = re.compile(r"\[RUBRIC\]\s+(\S+):\s*([0-9.]+)")
304
+ _RUBRIC_DERIVED_METRICS = {"weighted_score"}
305
+
306
+
307
+ def _rubric_scores(result: TreatmentResult) -> dict[str, float]:
308
+ """Extract per-dimension rubric scores from a run's passed checks."""
309
+ scores: dict[str, float] = {}
310
+ for check in result.checks_passed:
311
+ m = _RUBRIC_RE.search(check)
312
+ if m:
313
+ try:
314
+ scores[m.group(1)] = float(m.group(2))
315
+ except ValueError:
316
+ continue
317
+ return scores
318
+
319
+
320
+ def rubric_column(dim: str) -> ReportColumn:
321
+ """Column showing the score (0.00-1.00) for one rubric dimension."""
322
+
323
+ def extract(r: TreatmentResult) -> str:
324
+ scores = _rubric_scores(r)
325
+ return f"{scores.get(dim, 0):.2f}" if dim in scores else "/"
326
+
327
+ def aggregate(runs: list[TreatmentResult]) -> str:
328
+ vals = [_rubric_scores(r).get(dim) for r in runs]
329
+ vals = [v for v in vals if v is not None]
330
+ if not vals:
331
+ return "/"
332
+ return f"{sum(vals) / len(vals):.2f}"
333
+
334
+ return ReportColumn(name=dim, extract=extract, aggregate=aggregate)
335
+
336
+
337
+ def rubric_total_column() -> ReportColumn:
338
+ """Column showing the unweighted mean across base rubric dimensions."""
339
+
340
+ def total(scores: dict[str, float]) -> float | None:
341
+ base_scores = {
342
+ dim: score for dim, score in scores.items() if dim not in _RUBRIC_DERIVED_METRICS
343
+ }
344
+ if not base_scores:
345
+ return None
346
+ return sum(base_scores.values()) / len(base_scores)
347
+
348
+ def extract(r: TreatmentResult) -> str:
349
+ t = total(_rubric_scores(r))
350
+ return f"{t:.2f}" if t is not None else "/"
351
+
352
+ def aggregate(runs: list[TreatmentResult]) -> str:
353
+ per_run = [total(_rubric_scores(r)) for r in runs]
354
+ per_run = [t for t in per_run if t is not None]
355
+ if not per_run:
356
+ return "/"
357
+ return f"{sum(per_run) / len(per_run):.2f}"
358
+
359
+ return ReportColumn(name="RubricAvg", extract=extract, aggregate=aggregate)
360
+
361
+
362
+ def rubric_columns(dimensions: tuple[str, ...] | list[str] | None = None) -> list[ReportColumn]:
363
+ """Rubric dimension columns plus derived metrics and the aggregate."""
364
+ if dimensions is None:
365
+ from scaffold.python.profiles import all_rubric_dimensions
366
+
367
+ dimensions = all_rubric_dimensions()
368
+ base_dimensions = [dim for dim in dimensions if dim not in _RUBRIC_DERIVED_METRICS]
369
+ derived_metrics = [dim for dim in dimensions if dim in _RUBRIC_DERIVED_METRICS]
370
+ if "weighted_score" not in derived_metrics:
371
+ derived_metrics.append("weighted_score")
372
+ cols = [rubric_column(dim) for dim in base_dimensions]
373
+ cols.extend(rubric_column(dim) for dim in derived_metrics)
374
+ cols.append(rubric_total_column())
375
+ return cols
376
+
377
+ def _checks_aggregate(runs: list) -> str:
378
+ """Aggregate checks passed across runs."""
379
+ total_passed = sum(len(r.checks_passed) for r in runs)
380
+ total_checks = sum(len(r.checks_passed) + len(r.checks_failed) for r in runs)
381
+ pct = (total_passed / total_checks * 100) if total_checks > 0 else 0
382
+ return f"{total_passed}/{total_checks} ({pct:.0f}%)"
383
+
384
+
385
+ def _avg(values: list, fmt: str = "{:.1f}") -> str:
386
+ """Calculate average and format, or return N/A."""
387
+ values = [v for v in values if v is not None]
388
+ if not values:
389
+ return "N/A"
390
+ return fmt.format(sum(values) / len(values))
391
+
392
+
393
+ def _sum(values: list) -> int | float | None:
394
+ values = [v for v in values if v is not None]
395
+ return sum(values) if values else None
396
+
397
+
398
+ def _format_tokens(value: int | float | None) -> str:
399
+ if value is None:
400
+ return "N/A"
401
+ return f"{value:,.0f}"
402
+
403
+
404
+ def _format_cost(value: int | float | None) -> str:
405
+ if value is None:
406
+ return "N/A"
407
+ return f"${value:.4f}"
408
+
409
+
410
+ # =============================================================================
411
+ # EXPERIMENT LOGGER
412
+ # =============================================================================
413
+
414
+
415
+ class ExperimentLogger:
416
+ """Manages logging for a single experiment run."""
417
+
418
+ def __init__(
419
+ self,
420
+ experiment_name: str = None,
421
+ columns: list[ReportColumn] = None,
422
+ experiment_id: str = None,
423
+ report_outputs: ReportOutputConfig = None,
424
+ ):
425
+ """Create experiment logger.
426
+
427
+ Args:
428
+ experiment_name: Name for this experiment (used to generate ID if not provided)
429
+ columns: Custom columns for reporting (in addition to defaults)
430
+ experiment_id: Existing experiment ID to join (for parallel workers)
431
+ report_outputs: Enabled summary report formats
432
+ """
433
+ if experiment_id:
434
+ # Join existing experiment
435
+ self.experiment_id = experiment_id
436
+ self.name = experiment_id.rsplit("_", 2)[0] if "_" in experiment_id else experiment_id
437
+ self.timestamp = experiment_id.rsplit("_", 1)[-1] if "_" in experiment_id else ""
438
+ else:
439
+ # Create new experiment
440
+ self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
441
+ self.name = experiment_name or f"experiment_{self.timestamp}"
442
+ self.experiment_id = f"{self.name}_{self.timestamp}"
443
+
444
+ self.base_dir = get_logs_dir() / "experiments" / self.experiment_id
445
+
446
+ # Create subdirectories
447
+ self.events_dir = self.base_dir / "events"
448
+ self.reports_dir = self.base_dir / "reports"
449
+ self.raw_dir = self.base_dir / "raw"
450
+
451
+ for d in [self.events_dir, self.reports_dir, self.raw_dir]:
452
+ d.mkdir(parents=True, exist_ok=True)
453
+
454
+ self.columns = list(columns) if columns else rubric_columns()
455
+ self.report_outputs = report_outputs or ReportOutputConfig()
456
+ self.results: dict[str, list[TreatmentResult]] = {}
457
+ self.metadata: dict[str, Any] = {
458
+ "experiment_id": self.experiment_id,
459
+ "started_at": datetime.now().isoformat(),
460
+ "treatments": [],
461
+ }
462
+
463
+ def add_result(self, treatment_name: str, result: TreatmentResult):
464
+ """Add a treatment result."""
465
+ if treatment_name not in self.results:
466
+ self.results[treatment_name] = []
467
+ self.metadata["treatments"].append(treatment_name)
468
+ self.results[treatment_name].append(result)
469
+
470
+ def _get_all_columns(self) -> list[ReportColumn]:
471
+ """Get all columns: custom first, then defaults."""
472
+ defaults = default_columns()
473
+ pass_col = defaults[0]
474
+ metric_cols = defaults[1:]
475
+ return [pass_col] + self.columns + metric_cols
476
+
477
+ def _aggregate_by_base_treatment(self) -> dict[str, list[TreatmentResult]]:
478
+ """Group results by base treatment name (strip -N-M suffix from pytest --count).
479
+
480
+ Example: ADV_BASELINE-1-3, ADV_BASELINE-2-3, ADV_BASELINE-3-3 -> ADV_BASELINE
481
+ """
482
+ base_treatments: dict[str, list[TreatmentResult]] = {}
483
+ pattern = re.compile(r"^(.+)-\d+-\d+$") # Matches NAME-N-M
484
+
485
+ for name, runs in self.results.items():
486
+ match = pattern.match(name)
487
+ base_name = match.group(1) if match else name
488
+ if base_name not in base_treatments:
489
+ base_treatments[base_name] = []
490
+ base_treatments[base_name].extend(runs)
491
+
492
+ return base_treatments
493
+
494
+ def generate_summary(self) -> str:
495
+ """Generate markdown summary of experiment results."""
496
+ lines = []
497
+ lines.append("# Experiment Results Summary\n")
498
+ lines.append(f"**Experiment ID:** `{self.experiment_id}`\n")
499
+ lines.append(f"**Started:** {self.metadata['started_at']}\n")
500
+ lines.append(f"**Completed:** {datetime.now().isoformat()}\n")
501
+ lines.append("")
502
+
503
+ columns = self._get_all_columns()
504
+ has_reps = any(len(runs) > 1 for runs in self.results.values())
505
+
506
+ # Column definitions section
507
+ custom_cols = [c for c in self.columns if c.description]
508
+ if custom_cols:
509
+ lines.append("## Column Definitions\n")
510
+ for col in custom_cols:
511
+ lines.append(f"- **{col.name}**: {col.description}")
512
+ lines.append("")
513
+
514
+ col_names = ["Treatment"] + [c.name for c in columns]
515
+ header = "| " + " | ".join(col_names) + " |"
516
+ separator = "|" + "|".join("-" * (len(name) + 2) for name in col_names) + "|"
517
+
518
+ if has_reps:
519
+ lines.append("## Results (with Repetitions)\n")
520
+ else:
521
+ lines.append("## Results\n")
522
+
523
+ lines.append(header)
524
+ lines.append(separator)
525
+
526
+ for name, runs in self.results.items():
527
+ if has_reps:
528
+ values = [c.get_aggregate(runs) for c in columns]
529
+ else:
530
+ values = [c.get_value(runs[0]) for c in columns]
531
+ lines.append(f"| {name} | " + " | ".join(values) + " |")
532
+
533
+ lines.append("")
534
+
535
+ total_runs = sum(len(runs) for runs in self.results.values())
536
+ total_checks_passed = sum(
537
+ sum(len(r.checks_passed) for r in runs) for runs in self.results.values()
538
+ )
539
+ total_checks = sum(
540
+ sum(len(r.checks_passed) + len(r.checks_failed) for r in runs)
541
+ for runs in self.results.values()
542
+ )
543
+ check_pct = (total_checks_passed / total_checks * 100) if total_checks > 0 else 0
544
+
545
+ lines.append("## Summary\n")
546
+ lines.append(f"- **Total Runs:** {total_runs}")
547
+ lines.append(
548
+ f"- **Checks Passed:** {total_checks_passed}/{total_checks} ({check_pct:.1f}%)"
549
+ )
550
+ lines.append("")
551
+
552
+ # Aggregate by base treatment name (strip -N-M suffix from pytest --count)
553
+ base_treatments = self._aggregate_by_base_treatment()
554
+ if base_treatments and len(base_treatments) < len(self.results):
555
+ lines.append("## Aggregated by Treatment\n")
556
+ lines.append(
557
+ "| Treatment | Reps Passed | Checks | Avg Turns | Avg Duration | Tokens | Cost | Skills | Scripts |"
558
+ )
559
+ lines.append(
560
+ "|-----------|-------------|--------|-----------|--------------|--------|------|--------|---------|"
561
+ )
562
+ for base_name, all_runs in base_treatments.items():
563
+ # A rep passes if all checks pass (no failures)
564
+ reps_passed = sum(1 for r in all_runs if not r.checks_failed)
565
+ total_reps = len(all_runs)
566
+ total_passed = sum(len(r.checks_passed) for r in all_runs)
567
+ total_all = sum(len(r.checks_passed) + len(r.checks_failed) for r in all_runs)
568
+ pct = (total_passed / total_all * 100) if total_all > 0 else 0
569
+ avg_turns = _avg([r.turns for r in all_runs if r.turns], "{:.0f}")
570
+ avg_dur = _avg([r.duration for r in all_runs if r.duration], "{:.0f}s")
571
+ total_tokens = _format_tokens(_sum([r.total_tokens for r in all_runs]))
572
+ total_cost = _format_cost(_sum([r.total_cost_usd for r in all_runs]))
573
+ # Get skills/scripts from first run (should be same for all reps)
574
+ skills = (
575
+ ", ".join(all_runs[0].skills_invoked) if all_runs[0].skills_invoked else "none"
576
+ )
577
+ scripts = (
578
+ ", ".join(all_runs[0].scripts_used) if all_runs[0].scripts_used else "none"
579
+ )
580
+ lines.append(
581
+ f"| {base_name} | {reps_passed}/{total_reps} | {total_passed}/{total_all} ({pct:.0f}%) | {avg_turns} | {avg_dur} | {total_tokens} | {total_cost} | {skills} | {scripts} |"
582
+ )
583
+ lines.append("")
584
+
585
+ # Detailed per-treatment breakdown
586
+ lines.append("## Treatment Details\n")
587
+ for name, runs in self.results.items():
588
+ treatment_passed = sum(len(r.checks_passed) for r in runs)
589
+ treatment_total = sum(len(r.checks_passed) + len(r.checks_failed) for r in runs)
590
+ treatment_pct = (treatment_passed / treatment_total * 100) if treatment_total > 0 else 0
591
+ lines.append(
592
+ f"### {name} ({treatment_passed}/{treatment_total} checks, {treatment_pct:.0f}%)\n"
593
+ )
594
+
595
+ for i, r in enumerate(runs, 1):
596
+ run_label = f"Run {i}" if has_reps else "Result"
597
+ run_passed = len(r.checks_passed)
598
+ run_total = run_passed + len(r.checks_failed)
599
+ run_pct = (run_passed / run_total * 100) if run_total > 0 else 0
600
+ run_id_str = f" (run_id: {r.run_id})" if r.run_id else ""
601
+ lines.append(
602
+ f"**{run_label}:** {run_passed}/{run_total} checks ({run_pct:.0f}%){run_id_str}"
603
+ )
604
+
605
+ # Show metrics
606
+ metrics = []
607
+ if r.turns:
608
+ metrics.append(f"Turns: {r.turns}")
609
+ if r.duration:
610
+ metrics.append(f"Duration: {r.duration:.0f}s")
611
+ if r.tool_calls:
612
+ metrics.append(f"Tool calls: {r.tool_calls}")
613
+ if r.total_tokens is not None:
614
+ metrics.append(f"Tokens: {_format_tokens(r.total_tokens)}")
615
+ if r.total_cost_usd is not None:
616
+ metrics.append(f"Cost: {_format_cost(r.total_cost_usd)}")
617
+ if metrics:
618
+ lines.append(f"- Metrics: {', '.join(metrics)}")
619
+
620
+ # Show skills and scripts
621
+ if r.skills_invoked:
622
+ lines.append(f"- Skills invoked: {', '.join(r.skills_invoked)}")
623
+ if r.scripts_used:
624
+ lines.append(f"- Scripts used: {', '.join(r.scripts_used)}")
625
+
626
+ # Show all passed checks
627
+ if r.checks_passed:
628
+ lines.append(f"- Passed checks ({len(r.checks_passed)}):")
629
+ for check in r.checks_passed:
630
+ lines.append(f" - {check}")
631
+
632
+ # Show all failed checks
633
+ if r.checks_failed:
634
+ lines.append(f"- Failed checks ({len(r.checks_failed)}):")
635
+ for check in r.checks_failed:
636
+ lines.append(f" - {check}")
637
+
638
+ lines.append("")
639
+
640
+ return "\n".join(lines)
641
+
642
+ def finalize(self):
643
+ """Generate and save final summary."""
644
+ summary = self.generate_summary()
645
+ summary_path = self.base_dir / "summary.md"
646
+ written = write_report_outputs(
647
+ summary,
648
+ summary_path,
649
+ self.report_outputs,
650
+ title="Comet Eval Summary",
651
+ )
652
+
653
+ self.metadata["completed_at"] = datetime.now().isoformat()
654
+ self.metadata["total_runs"] = sum(len(runs) for runs in self.results.values())
655
+ self.metadata["total_passed"] = sum(
656
+ sum(1 for r in runs if r.passed) for runs in self.results.values()
657
+ )
658
+ self.metadata["report_outputs"] = {name: str(path) for name, path in written.items()}
659
+
660
+ metadata_path = self.base_dir / "metadata.json"
661
+ metadata_path.write_text(json.dumps(self.metadata, indent=2), encoding="utf-8")
662
+
663
+ print(f"\nExperiment results saved to: {self.base_dir}")
664
+ if written:
665
+ print("Summary: " + ", ".join(str(path) for path in written.values()))
666
+ else:
667
+ print("Summary outputs disabled by report config")
668
+
669
+ return preferred_report_path(written, self.base_dir)
670
+
671
+
672
+ # =============================================================================
673
+ # PARALLEL SAVE HELPERS (for multiprocessing workers)
674
+ # =============================================================================
675
+
676
+
677
+ def save_events(base_dir: Path, treatment_name: str, rep: int, events: dict[str, Any]):
678
+ """Save events JSON."""
679
+ events_dir = base_dir / "events"
680
+ events_dir.mkdir(parents=True, exist_ok=True)
681
+ save_path = events_dir / f"{stable_treatment_name(treatment_name)}_rep{rep}.json"
682
+ save_path.write_text(json.dumps(events, indent=2), encoding="utf-8")
683
+ return save_path
684
+
685
+
686
+ def save_raw(base_dir: Path, treatment_name: str, rep: int, stdout: str, stderr: str = None):
687
+ """Save raw CLI output."""
688
+ raw_dir = base_dir / "raw"
689
+ raw_dir.mkdir(parents=True, exist_ok=True)
690
+ stable_name = stable_treatment_name(treatment_name)
691
+ stdout_path = raw_dir / f"{stable_name}_rep{rep}_stdout.json"
692
+ stdout_path.write_text(stdout, encoding="utf-8")
693
+
694
+ if stderr:
695
+ stderr_path = raw_dir / f"{stable_name}_rep{rep}_stderr.txt"
696
+ stderr_path.write_text(stderr, encoding="utf-8")
697
+
698
+
699
+ def save_report(base_dir: Path, treatment_name: str, rep: int, report: dict[str, Any]):
700
+ """Save treatment report."""
701
+ reports_dir = base_dir / "reports"
702
+ reports_dir.mkdir(parents=True, exist_ok=True)
703
+ save_path = reports_dir / f"{stable_treatment_name(treatment_name)}_rep{rep}_report.json"
704
+ save_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
705
+ return save_path