@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,1148 @@
1
+ """Shared pytest fixtures and experiment logging plugin.
2
+
3
+ Generates rich experiment logs in logs/experiments/ including:
4
+ - summary.md: Full markdown report with tables and details
5
+ - events/: Parsed events from each test run
6
+ - raw/: Raw Claude CLI output
7
+ - reports/: Per-run validation reports
8
+ - artifacts/: Files Claude generated and their execution output
9
+ - metadata.json: Experiment metadata
10
+
11
+ Supports pytest-xdist parallel execution via worker coordination.
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import tempfile
19
+ import time
20
+ import uuid
21
+ from contextlib import contextmanager
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+ from types import SimpleNamespace
25
+ from typing import Any
26
+
27
+ import pytest
28
+ from dotenv import load_dotenv
29
+
30
+ from scaffold import run_claude_in_docker, run_node_in_docker, run_python_in_docker, run_shell
31
+ from scaffold.python import (
32
+ ExperimentLogger,
33
+ TreatmentResult,
34
+ build_eval_artifact_references,
35
+ classify_failures,
36
+ get_profile,
37
+ load_report_output_config,
38
+ save_events,
39
+ save_raw,
40
+ save_report,
41
+ strip_ansi,
42
+ )
43
+ from scaffold.python.sample_quality import infer_sample_quality
44
+ from scaffold.python.skill_parser import SCRIPT_EXTENSIONS
45
+
46
+ # =============================================================================
47
+ # CONSTANTS
48
+ # =============================================================================
49
+
50
+ PROJECT_ROOT = Path(__file__).parent.parent
51
+ EVAL_ROOT = PROJECT_ROOT.parent
52
+
53
+ # Shared files for xdist worker coordination
54
+ XDIST_EXPERIMENT_FILE = PROJECT_ROOT / ".pytest_experiment_id"
55
+ DOCKER_BUILD_LOCK = PROJECT_ROOT / ".pytest_docker_build.lock"
56
+
57
+ # Global plugin instance (set during pytest_configure)
58
+ _plugin: "ExperimentPlugin | None" = None
59
+
60
+ # Cache discovered scripts (computed once on first call)
61
+ _KNOWN_SCRIPTS: list[str] | None = None
62
+
63
+ COMET_WORKFLOW_CLAUDE_MD_PATH = (
64
+ PROJECT_ROOT
65
+ / "skills"
66
+ / "benchmarks"
67
+ / "dependency"
68
+ / "claude-md"
69
+ / "comet-workflow"
70
+ / "CLAUDE.md"
71
+ )
72
+
73
+
74
+ # =============================================================================
75
+ # PYTEST HOOKS
76
+ # =============================================================================
77
+
78
+
79
+ def pytest_addoption(parser):
80
+ """Add CLI options for task and treatment selection."""
81
+ parser.addoption(
82
+ "--task",
83
+ action="store",
84
+ default=None,
85
+ help="Run specific task (e.g., --task=comet-full-workflow)",
86
+ )
87
+ parser.addoption(
88
+ "--treatment",
89
+ action="store",
90
+ default=None,
91
+ help="Run specific treatment (e.g., --treatment=COMET_FULL_040_BETA)",
92
+ )
93
+ parser.addoption(
94
+ "--count",
95
+ action="store",
96
+ type=int,
97
+ default=1,
98
+ help="Repeat each task/treatment combination N times for distribution stats (default: 1)",
99
+ )
100
+ parser.addoption(
101
+ "--skill-path",
102
+ action="store",
103
+ default=None,
104
+ help="Local Skill directory or SKILL.md to evaluate",
105
+ )
106
+ parser.addoption(
107
+ "--skill-name",
108
+ action="store",
109
+ default=None,
110
+ help="Skill name to inject for --skill-path",
111
+ )
112
+ parser.addoption(
113
+ "--profile",
114
+ action="store",
115
+ default=None,
116
+ help="Eval profile override",
117
+ )
118
+ parser.addoption(
119
+ "--eval-manifest",
120
+ action="store",
121
+ default=None,
122
+ help="Path to comet/eval.yaml",
123
+ )
124
+ parser.addoption(
125
+ "--interaction-mode",
126
+ action="store",
127
+ default=None,
128
+ help="Override interaction mode (e.g., none, auto_user)",
129
+ )
130
+ parser.addoption(
131
+ "--max-turns",
132
+ action="store",
133
+ default=None,
134
+ help="Override max interaction turns for auto_user loops",
135
+ )
136
+ parser.addoption(
137
+ "--simulator-prompt",
138
+ action="store",
139
+ default=None,
140
+ help="Override user simulator prompt for auto_user loops",
141
+ )
142
+ parser.addoption(
143
+ "--report-config",
144
+ action="store",
145
+ default=None,
146
+ help="JSON/YAML config for eval report outputs",
147
+ )
148
+
149
+
150
+ def pytest_configure(config):
151
+ """Register experiment plugin (decision deferred to sessionstart)."""
152
+ global _plugin
153
+ _plugin = ExperimentPlugin(config)
154
+ config.pluginmanager.register(_plugin, "experiment_plugin")
155
+
156
+
157
+ # =============================================================================
158
+ # EXPERIMENT PLUGIN
159
+ # =============================================================================
160
+
161
+
162
+ class ExperimentPlugin:
163
+ """Pytest plugin that generates rich experiment logs in logs/experiments/."""
164
+
165
+ def __init__(self, config):
166
+ self.config = config
167
+ self.logger: ExperimentLogger | None = None
168
+ self.start_time = None
169
+ self.run_counter: dict[str, int] = {}
170
+ self.is_xdist_worker = hasattr(config, "workerinput")
171
+ self.is_xdist_master = (
172
+ not hasattr(config, "workerinput")
173
+ and (getattr(config.option, "numprocesses", None) or 0) > 0
174
+ )
175
+ self.worker_id = (
176
+ config.workerinput.get("workerid", "master") if self.is_xdist_worker else "master"
177
+ )
178
+
179
+ def pytest_sessionstart(self, session):
180
+ """Create or join experiment logger at session start."""
181
+ if _is_unit_tests_only(self.config):
182
+ return
183
+
184
+ name = _get_experiment_name(session)
185
+ use_coordination = self.is_xdist_worker or self.is_xdist_master
186
+ experiment_id = _get_or_create_experiment_id(name, use_coordination)
187
+
188
+ report_outputs = load_report_output_config(self.config.getoption("--report-config"))
189
+ self.logger = ExperimentLogger(
190
+ experiment_name=name,
191
+ experiment_id=experiment_id,
192
+ report_outputs=report_outputs,
193
+ )
194
+ self.start_time = time.time()
195
+
196
+ print(f"\n{'=' * 60}")
197
+ print(f"EXPERIMENT: {self.logger.experiment_id}")
198
+ print(f"Logging to: {self.logger.base_dir}")
199
+ print(f"{'=' * 60}\n")
200
+
201
+ def pytest_sessionfinish(self, session, exitstatus):
202
+ """Generate and save summary at session end."""
203
+ if not self.logger:
204
+ return
205
+
206
+ if self.is_xdist_worker:
207
+ return
208
+
209
+ if self.is_xdist_master:
210
+ time.sleep(1)
211
+
212
+ self._reload_results_from_reports()
213
+
214
+ if self.logger.results:
215
+ self.logger.finalize()
216
+ self._print_summary()
217
+
218
+ _cleanup_experiment_coordination()
219
+
220
+ def get_rep_number(self, treatment_name: str) -> int:
221
+ """Get the next repetition number for a treatment."""
222
+ if treatment_name not in self.run_counter:
223
+ self.run_counter[treatment_name] = 0
224
+ self.run_counter[treatment_name] += 1
225
+ return self.run_counter[treatment_name]
226
+
227
+ def _reload_results_from_reports(self):
228
+ """Reload results from saved report files (aggregates all workers)."""
229
+ reports_dir = self.logger.reports_dir
230
+ if not reports_dir.exists():
231
+ return
232
+
233
+ self.logger.results.clear()
234
+
235
+ for report_file in sorted(reports_dir.glob("*.json")):
236
+ try:
237
+ report = json.loads(report_file.read_text())
238
+ treatment_name = report.get("name", "unknown")
239
+ result = TreatmentResult(
240
+ name=treatment_name,
241
+ passed=report.get("passed", False),
242
+ checks_passed=report.get("checks_passed", []),
243
+ checks_failed=report.get("checks_failed", []),
244
+ events_summary=report.get("events_summary", {}),
245
+ run_id=report.get("run_id", ""),
246
+ )
247
+ if treatment_name not in self.logger.results:
248
+ self.logger.results[treatment_name] = []
249
+ self.logger.results[treatment_name].append(result)
250
+ except Exception as e:
251
+ import sys
252
+
253
+ print(f"Warning: failed to reload report {report_file.name}: {e}", file=sys.stderr)
254
+
255
+ def _print_summary(self):
256
+ """Print summary to console."""
257
+ print(f"\n{'=' * 120}")
258
+ print(" RESULTS")
259
+ print(f"{'=' * 120}\n")
260
+
261
+ print(
262
+ f"{'Treatment':<25} {'Checks':<15} {'Turns':<8} {'Dur':<8} {'Tokens':<12} {'Cost':<10} {'Skills':<40}"
263
+ )
264
+ print("-" * 120)
265
+
266
+ for treatment, runs in self.logger.results.items():
267
+ for r in runs:
268
+ checks_passed = len(r.checks_passed)
269
+ checks_total = checks_passed + len(r.checks_failed)
270
+ check_pct = (checks_passed / checks_total * 100) if checks_total > 0 else 0
271
+ checks_str = f"{checks_passed}/{checks_total} ({check_pct:.0f}%)"
272
+ turns = str(r.turns) if r.turns else "?"
273
+ dur = f"{r.duration:.0f}s" if r.duration else "?"
274
+ tokens = f"{r.total_tokens:,}" if r.total_tokens is not None else "?"
275
+ cost = f"${r.total_cost_usd:.4f}" if r.total_cost_usd is not None else "?"
276
+ skills = r.events_summary.get("skills_invoked", [])
277
+ skills_str = ", ".join(skills) if skills else "none"
278
+ if len(skills_str) > 38:
279
+ skills_str = skills_str[:35] + "..."
280
+ print(
281
+ f"{treatment:<25} {checks_str:<15} {turns:<8} {dur:<8} {tokens:<12} {cost:<10} {skills_str:<40}"
282
+ )
283
+
284
+ print("-" * 120)
285
+ total_passed = sum(
286
+ sum(len(r.checks_passed) for r in runs) for runs in self.logger.results.values()
287
+ )
288
+ total_checks = sum(
289
+ sum(len(r.checks_passed) + len(r.checks_failed) for r in runs)
290
+ for runs in self.logger.results.values()
291
+ )
292
+ if total_checks:
293
+ print(
294
+ f"Total: {total_passed}/{total_checks} checks passed ({total_passed / total_checks * 100:.1f}%)"
295
+ )
296
+ print(f"{'=' * 120}")
297
+
298
+
299
+ # =============================================================================
300
+ # EXPERIMENT PLUGIN HELPERS
301
+ # =============================================================================
302
+
303
+
304
+ def _is_unit_tests_only(config) -> bool:
305
+ """Check if running ONLY unit tests (scaffold/scripts - don't need experiment logs)."""
306
+ args = [a for a in (config.args or []) if not a.startswith("-")]
307
+ if not args:
308
+ return False
309
+ return all("scripts" in arg or "scaffold" in arg for arg in args)
310
+
311
+
312
+ def _get_experiment_name(session) -> str:
313
+ """Determine experiment name from task name parameter."""
314
+ items = getattr(session, "items", None)
315
+ if not items:
316
+ return "experiment"
317
+
318
+ first_item = items[0]
319
+ if hasattr(first_item, "callspec") and "task_name" in first_item.callspec.params:
320
+ return first_item.callspec.params["task_name"].replace("-", "_")
321
+
322
+ return "experiment"
323
+
324
+
325
+ def _get_dynamic_treatment_config(config):
326
+ manifest_path = config.getoption("--eval-manifest")
327
+ if manifest_path:
328
+ from scaffold.python.manifests import load_eval_manifest
329
+ from scaffold.python.treatments import TreatmentConfig
330
+
331
+ manifest = load_eval_manifest(manifest_path)
332
+ node_skills = []
333
+ for node_skill in manifest.generated_node_skills:
334
+ node_path = manifest.skill_path.parent / node_skill
335
+ if (node_path / "SKILL.md").exists():
336
+ node_skills.append(
337
+ {
338
+ "name": node_skill,
339
+ "source": "path",
340
+ "path": str(node_path),
341
+ }
342
+ )
343
+ return TreatmentConfig(
344
+ name="DYNAMIC_SKILL",
345
+ description=f"Dynamic Skill target: {manifest.skill_name}",
346
+ skills=[
347
+ {
348
+ "name": manifest.skill_name,
349
+ "source": "path",
350
+ "path": str(manifest.skill_path),
351
+ "profile": manifest.profile,
352
+ "manifest": str(manifest.path),
353
+ "baseline_treatments": manifest.baseline_treatments,
354
+ "quality_gates": manifest.quality_gates,
355
+ "required_output_schemas": manifest.required_output_schemas,
356
+ "expected_evidence": manifest.expected_evidence,
357
+ "draft_hash": manifest.draft_hash,
358
+ "required_skills": manifest.required_skills,
359
+ "expected_artifacts": manifest.expected_artifacts,
360
+ "generated_node_skills": manifest.generated_node_skills,
361
+ "route_conformance_task": manifest.route_conformance_task,
362
+ "route_conformance_expected_node_order": (
363
+ manifest.route_conformance_expected_node_order
364
+ ),
365
+ }
366
+ ]
367
+ + node_skills,
368
+ )
369
+
370
+ skill_path = config.getoption("--skill-path")
371
+ if not skill_path:
372
+ return None
373
+ skill_name = config.getoption("--skill-name") or Path(skill_path).resolve().parent.name
374
+ profile = config.getoption("--profile")
375
+ skill_cfg = {
376
+ "name": skill_name,
377
+ "source": "path",
378
+ "path": skill_path,
379
+ }
380
+ if profile:
381
+ skill_cfg["profile"] = profile
382
+ from scaffold.python.treatments import TreatmentConfig
383
+
384
+ return TreatmentConfig(
385
+ name="DYNAMIC_SKILL",
386
+ description=f"Dynamic Skill target: {skill_name}",
387
+ skills=[skill_cfg],
388
+ )
389
+
390
+
391
+ def _resolve_interaction_config(task, profile_name: str, config):
392
+ profile_default = get_profile(profile_name).default_interaction
393
+ task_interaction = task.config.interaction
394
+
395
+ mode = task_interaction.mode or profile_default.mode
396
+ max_turns = task_interaction.max_turns or profile_default.max_turns
397
+ simulator_prompt = task_interaction.simulator_prompt or profile_default.simulator_prompt
398
+ decision_patterns = list(task_interaction.decision_patterns or profile_default.decision_patterns)
399
+ continue_prompt = task_interaction.continue_prompt or profile_default.continue_prompt
400
+
401
+ mode_override = config.getoption("--interaction-mode")
402
+ if mode_override:
403
+ mode = mode_override
404
+
405
+ max_turns_override = config.getoption("--max-turns")
406
+ if max_turns_override not in (None, ""):
407
+ max_turns = int(max_turns_override)
408
+
409
+ simulator_prompt_override = config.getoption("--simulator-prompt")
410
+
411
+ prompt_file = os.environ.get("BENCH_SIMULATOR_PROMPT_FILE")
412
+ prompt_path = Path(prompt_file) if prompt_file else (EVAL_ROOT / "simulator-instruction.md")
413
+ if not prompt_path.is_absolute():
414
+ prompt_path = EVAL_ROOT / prompt_path
415
+ if prompt_path.exists():
416
+ simulator_prompt = prompt_path.read_text(encoding="utf-8")
417
+
418
+ if simulator_prompt_override:
419
+ simulator_prompt = simulator_prompt_override
420
+
421
+ return task_interaction.__class__(
422
+ mode=mode,
423
+ max_turns=max_turns,
424
+ simulator_prompt=simulator_prompt,
425
+ decision_patterns=decision_patterns,
426
+ continue_prompt=continue_prompt,
427
+ )
428
+
429
+
430
+ def _read_required_text(path: Path) -> str:
431
+ if not path.exists():
432
+ raise FileNotFoundError(f"Required eval instruction file not found: {path}")
433
+ return path.read_text(encoding="utf-8")
434
+
435
+
436
+ def _build_eval_claude_md(profile_name: str, treatment_claude_md: str | None = None) -> str | None:
437
+ sections: list[str] = []
438
+ if profile_name == "comet-workflow":
439
+ sections.append(_read_required_text(COMET_WORKFLOW_CLAUDE_MD_PATH))
440
+ if treatment_claude_md:
441
+ sections.append(treatment_claude_md.strip())
442
+ return "\n\n".join(section for section in sections if section.strip()) or None
443
+
444
+
445
+ def _comet_hook_command(test_dir: Path) -> str | None:
446
+ scripts_dir = test_dir / ".claude" / "skills" / "comet" / "scripts"
447
+ mjs_hook = scripts_dir / "comet-hook-guard.mjs"
448
+ shell_hook = scripts_dir / "comet-hook-guard.sh"
449
+ if mjs_hook.exists():
450
+ return "node /workspace/.claude/skills/comet/scripts/comet-hook-guard.mjs"
451
+ if shell_hook.exists():
452
+ return "bash /workspace/.claude/skills/comet/scripts/comet-hook-guard.sh"
453
+ return None
454
+
455
+
456
+ def _ensure_claude_pre_tool_hook(test_dir: Path, command: str | None) -> None:
457
+ if not command:
458
+ return
459
+ settings_path = test_dir / ".claude" / "settings.json"
460
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
461
+ if settings_path.exists():
462
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
463
+ else:
464
+ settings = {}
465
+ hooks = settings.setdefault("hooks", {})
466
+ pre_tool_use = hooks.setdefault("PreToolUse", [])
467
+ hook_entry = {
468
+ "matcher": "Write|Edit|MultiEdit",
469
+ "hooks": [{"type": "command", "command": command}],
470
+ }
471
+ if not any(entry == hook_entry for entry in pre_tool_use):
472
+ pre_tool_use.append(hook_entry)
473
+ settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
474
+
475
+
476
+ def _get_or_create_experiment_id(name: str, use_coordination: bool) -> str:
477
+ """Get shared experiment ID or create new one."""
478
+ if not use_coordination:
479
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
480
+ return f"{name}_{timestamp}"
481
+
482
+ lock_file = XDIST_EXPERIMENT_FILE.with_suffix(".lock")
483
+
484
+ with file_lock(lock_file):
485
+ if XDIST_EXPERIMENT_FILE.exists():
486
+ data = json.loads(XDIST_EXPERIMENT_FILE.read_text())
487
+ return data["experiment_id"]
488
+
489
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
490
+ experiment_id = f"{name}_{timestamp}"
491
+ XDIST_EXPERIMENT_FILE.write_text(
492
+ json.dumps(
493
+ {
494
+ "experiment_id": experiment_id,
495
+ "created_at": datetime.now().isoformat(),
496
+ }
497
+ )
498
+ )
499
+ return experiment_id
500
+
501
+
502
+ def _snapshot_dynamic_skill_package(test_dir: Path, skill_hints: dict[str, Any]) -> str | None:
503
+ """Copy a manifest target package and generated Node Skills into the workspace.
504
+
505
+ Validation scripts run inside Docker with ``test_dir`` mounted as /workspace,
506
+ so they cannot inspect arbitrary host paths from the original manifest. Keep
507
+ a package-shaped snapshot under the workspace and pass a relative path.
508
+ """
509
+ raw_path = skill_hints.get("path")
510
+ if not raw_path:
511
+ return None
512
+ source = Path(raw_path).expanduser().resolve()
513
+ package_dir = source.parent if source.is_file() else source
514
+ if not package_dir.exists():
515
+ return None
516
+
517
+ snapshot_root = test_dir / "_eval_target_skills"
518
+ snapshot_root.mkdir(parents=True, exist_ok=True)
519
+ package_dest = snapshot_root / package_dir.name
520
+ shutil.copytree(package_dir, package_dest, dirs_exist_ok=True)
521
+
522
+ for node_skill in skill_hints.get("generated_node_skills") or []:
523
+ node_source = package_dir.parent / node_skill
524
+ if not (node_source / "SKILL.md").exists():
525
+ continue
526
+ shutil.copytree(node_source, snapshot_root / node_skill, dirs_exist_ok=True)
527
+
528
+ return str(package_dest.relative_to(test_dir)).replace("\\", "/")
529
+
530
+
531
+ def _cleanup_experiment_coordination():
532
+ """Remove coordination files after experiment."""
533
+ import sys
534
+
535
+ for f in [XDIST_EXPERIMENT_FILE, XDIST_EXPERIMENT_FILE.with_suffix(".lock")]:
536
+ try:
537
+ f.unlink(missing_ok=True)
538
+ except Exception as e:
539
+ print(f"Warning: failed to clean up {f.name}: {e}", file=sys.stderr)
540
+
541
+
542
+ # =============================================================================
543
+ # SESSION-SCOPED FIXTURES
544
+ # =============================================================================
545
+
546
+
547
+ @pytest.fixture(scope="session")
548
+ def project_root():
549
+ """Project root directory."""
550
+ return PROJECT_ROOT
551
+
552
+
553
+ @pytest.fixture(scope="session")
554
+ def worker_id(request):
555
+ """Get pytest-xdist worker ID, or 'master' if not using xdist."""
556
+ if hasattr(request.config, "workerinput"):
557
+ return request.config.workerinput["workerid"]
558
+ return "master"
559
+
560
+
561
+ @pytest.fixture(scope="session", autouse=True)
562
+ def verify_environment(project_root, request):
563
+ """Verify Docker, Claude CLI, uv, bash, and API keys are available."""
564
+ if _is_unit_tests_only(request.config):
565
+ return
566
+
567
+ load_dotenv(EVAL_ROOT / ".env")
568
+ load_dotenv(project_root / ".env")
569
+
570
+ # Check uv (Python package manager)
571
+ if shutil.which("uv") is None:
572
+ pytest.skip(
573
+ "uv is not installed or not in PATH.\n"
574
+ "Install it: https://docs.astral.sh/uv/getting-started/installation/"
575
+ )
576
+
577
+ # Check bash (required for MSYS shell scripts on Windows)
578
+ from scaffold.python.utils import BASH_EXEC
579
+
580
+ if os.name == "nt" and BASH_EXEC == "bash":
581
+ # _resolve_bash() fell back to bare "bash" — verify it actually works
582
+ try:
583
+ bash_check = subprocess.run(
584
+ ["bash", "--version"], capture_output=True, timeout=5
585
+ )
586
+ if bash_check.returncode != 0:
587
+ raise FileNotFoundError
588
+ except (FileNotFoundError, subprocess.TimeoutExpired):
589
+ pytest.skip(
590
+ "Git Bash not found. Install Git for Windows: https://git-scm.com/download/win\n"
591
+ "Or set GIT_BASH env var to the full path of bash.exe"
592
+ )
593
+
594
+ result = run_shell("docker.sh", "check", check=False)
595
+ if result.returncode != 0:
596
+ pytest.skip("Docker not available")
597
+
598
+ if not (os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN")):
599
+ pytest.skip("ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN not set")
600
+
601
+ if shutil.which("claude") is None:
602
+ pytest.skip("Claude CLI not available")
603
+
604
+
605
+ @pytest.fixture(scope="session", autouse=True)
606
+ def prebuild_docker_image(request):
607
+ """Pre-build Docker image once per session to avoid race conditions."""
608
+ if _is_unit_tests_only(request.config):
609
+ yield
610
+ return
611
+
612
+ tasks_dir = PROJECT_ROOT / "tasks"
613
+ if tasks_dir.exists():
614
+ for task_dir in tasks_dir.iterdir():
615
+ if task_dir.is_dir():
616
+ env_dir = task_dir / "environment"
617
+ if env_dir.exists() and (env_dir / "Dockerfile").exists():
618
+ image = _build_docker_image_with_lock(env_dir)
619
+ if image:
620
+ print(f"\nPre-built Docker image: {image}")
621
+
622
+ yield
623
+
624
+ try:
625
+ DOCKER_BUILD_LOCK.unlink(missing_ok=True)
626
+ except Exception:
627
+ pass
628
+
629
+
630
+ # =============================================================================
631
+ # FUNCTION-SCOPED FIXTURES
632
+ # =============================================================================
633
+
634
+
635
+ @pytest.fixture
636
+ def test_dir(tmp_path):
637
+ """Create isolated test directory (pytest manages cleanup)."""
638
+ return tmp_path
639
+
640
+
641
+ @pytest.fixture
642
+ def experiment_logger():
643
+ """Get the experiment logger for the current session."""
644
+ return _plugin.logger if _plugin else None
645
+
646
+
647
+ @pytest.fixture
648
+ def setup_test_context(test_dir):
649
+ """Factory fixture to set up test context with skills and CLAUDE.md."""
650
+
651
+ def _write_skill(
652
+ skill_name: str,
653
+ skill_file: str,
654
+ scripts_dir: Path | None = None,
655
+ source_dir: Path | None = None,
656
+ ) -> None:
657
+ skill_dir = test_dir / ".claude" / "skills" / skill_name
658
+ skill_dir.mkdir(parents=True, exist_ok=True)
659
+
660
+ if source_dir and source_dir.is_dir():
661
+ shutil.copytree(source_dir, skill_dir, dirs_exist_ok=True)
662
+
663
+ shutil.copyfile(skill_file, skill_dir / "SKILL.md")
664
+
665
+ if scripts_dir and scripts_dir.is_dir():
666
+ scripts_dest = skill_dir / "scripts"
667
+ shutil.rmtree(scripts_dest, ignore_errors=True)
668
+ shutil.copytree(scripts_dir, scripts_dest, dirs_exist_ok=True)
669
+
670
+ def _copy_environment(environment_dir: Path) -> None:
671
+ for item in environment_dir.iterdir():
672
+ dest = test_dir / item.name
673
+ if item.is_dir():
674
+ if dest.exists() and dest.is_dir():
675
+ shutil.copytree(item, dest, dirs_exist_ok=True)
676
+ else:
677
+ shutil.copytree(item, dest)
678
+ else:
679
+ dest.parent.mkdir(parents=True, exist_ok=True)
680
+ shutil.copy2(item, dest)
681
+
682
+ def _write_claude_md(content_file: str) -> None:
683
+ claude_dir = test_dir / ".claude"
684
+ claude_dir.mkdir(parents=True, exist_ok=True)
685
+ shutil.copyfile(content_file, claude_dir / "CLAUDE.md")
686
+ shutil.copyfile(content_file, test_dir / "CLAUDE.md")
687
+
688
+ def _setup(skills: dict = None, claude_md: str = None, environment_dir: Path = None):
689
+ for skill_name, cfg in (skills or {}).items():
690
+ if not cfg:
691
+ continue
692
+
693
+ if isinstance(cfg, dict):
694
+ sections = cfg.get("sections") or cfg.get("all", [])
695
+ scripts_dir = cfg.get("scripts_dir")
696
+ script_filter = cfg.get("script_filter")
697
+ source_dir = cfg.get("source_dir")
698
+ else:
699
+ sections, scripts_dir, script_filter, source_dir = cfg, None, None, None
700
+
701
+ if not sections:
702
+ continue
703
+
704
+ content = "\n\n".join(s for s in sections if s and s.strip())
705
+ with tempfile.NamedTemporaryFile(
706
+ mode="w", suffix=".md", delete=False, encoding="utf-8"
707
+ ) as f:
708
+ f.write(content)
709
+ skill_file = f.name
710
+
711
+ filtered_dir = _filter_scripts(scripts_dir, script_filter)
712
+ is_temp_dir = filtered_dir and filtered_dir != scripts_dir
713
+
714
+ try:
715
+ _write_skill(skill_name, skill_file, filtered_dir, source_dir)
716
+ finally:
717
+ os.unlink(skill_file)
718
+ if is_temp_dir and filtered_dir.exists():
719
+ shutil.rmtree(filtered_dir)
720
+
721
+ if environment_dir and environment_dir.exists():
722
+ _copy_environment(environment_dir)
723
+
724
+ if claude_md:
725
+ with tempfile.NamedTemporaryFile(
726
+ mode="w", suffix=".md", delete=False, encoding="utf-8"
727
+ ) as f:
728
+ f.write(claude_md)
729
+ temp_file = f.name
730
+ try:
731
+ _write_claude_md(temp_file)
732
+ finally:
733
+ os.unlink(temp_file)
734
+
735
+ _ensure_claude_pre_tool_hook(test_dir, _comet_hook_command(test_dir))
736
+
737
+ return test_dir
738
+
739
+ return _setup
740
+
741
+
742
+ @pytest.fixture
743
+ def run_claude(test_dir, experiment_logger, request):
744
+ """Factory fixture to run Claude in Docker and capture artifacts.
745
+
746
+ For tasks using ``interaction.mode=auto_user`` the single-shot ``run-claude``
747
+ is replaced by the multi-turn ``run-claude-loop`` driver, which simulates a
748
+ user replying at the workflow's decision points.
749
+ """
750
+ default_model = os.environ.get("BENCH_CC_MODEL")
751
+
752
+ def _run(prompt: str, timeout: int = 600, model: str = None, interaction=None):
753
+ mdl = model or default_model
754
+ if (
755
+ os.environ.get("TRACE_TO_LANGSMITH", "").lower() == "true"
756
+ and not os.environ.get("CC_LANGSMITH_LOG_FILE")
757
+ ):
758
+ os.environ["CC_LANGSMITH_LOG_FILE"] = "/workspace/langsmith-hook.log"
759
+ use_loop = interaction is not None and interaction.mode == "auto_user"
760
+ if not use_loop:
761
+ result = run_claude_in_docker(test_dir, prompt, timeout=timeout, model=mdl)
762
+ else:
763
+ task_prompt_file = test_dir / ".eval-task-prompt.txt"
764
+ task_prompt_file.write_text(prompt, encoding="utf-8")
765
+ loop_args = [
766
+ "run-claude-loop",
767
+ test_dir,
768
+ "@/workspace/.eval-task-prompt.txt",
769
+ "--max-turns",
770
+ str(interaction.max_turns),
771
+ ]
772
+ if mdl:
773
+ loop_args += ["--model", mdl]
774
+ if interaction.continue_prompt:
775
+ loop_args += ["--continue-prompt", interaction.continue_prompt]
776
+ for pattern in interaction.decision_patterns:
777
+ loop_args += ["--decision-pattern", pattern]
778
+
779
+ prompt_file = None
780
+ try:
781
+ if interaction.simulator_prompt:
782
+ prompt_file = test_dir / ".eval-simulator-prompt.txt"
783
+ prompt_file.write_text(interaction.simulator_prompt, encoding="utf-8")
784
+ loop_args += ["--simulator-prompt-file", "/workspace/.eval-simulator-prompt.txt"]
785
+ result = run_shell("docker.sh", *loop_args, timeout=timeout + 60, check=False)
786
+ finally:
787
+ task_prompt_file.unlink(missing_ok=True)
788
+ if prompt_file and prompt_file.exists():
789
+ prompt_file.unlink()
790
+
791
+ if experiment_logger and hasattr(request, "node"):
792
+ treatment_name = _get_treatment_name(request.node)
793
+ rep = _plugin.get_rep_number(treatment_name) if _plugin else 1
794
+ save_raw(
795
+ experiment_logger.base_dir,
796
+ treatment_name,
797
+ rep,
798
+ result.stdout,
799
+ result.stderr,
800
+ )
801
+
802
+ return result
803
+
804
+ return _run
805
+
806
+
807
+ @pytest.fixture
808
+ def record_result(test_dir, experiment_logger, request):
809
+ """Factory fixture to record validation results and save artifacts."""
810
+
811
+ def _record(
812
+ events: dict[str, Any],
813
+ passed: list[str],
814
+ failed: list[str],
815
+ run_id: str = "",
816
+ returncode: int | None = None,
817
+ stdout: str | None = None,
818
+ stderr: str | None = None,
819
+ ):
820
+ if not experiment_logger:
821
+ return
822
+
823
+ treatment_name = _get_treatment_name(request.node)
824
+ rep = _plugin.run_counter.get(treatment_name, 1) if _plugin else 1
825
+ base_dir = experiment_logger.base_dir
826
+
827
+ save_events(base_dir, treatment_name, rep, events)
828
+ _save_artifacts(base_dir, treatment_name, rep, test_dir)
829
+ artifact_references = build_eval_artifact_references(base_dir, treatment_name, rep)
830
+
831
+ scripts_used = _extract_scripts_used(events)
832
+ failure_attribution = classify_failures(
833
+ failed,
834
+ events,
835
+ events.get("profile"),
836
+ )
837
+
838
+ report = _build_report_payload(
839
+ treatment_name=treatment_name,
840
+ rep=rep,
841
+ run_id=run_id,
842
+ events=events,
843
+ passed=passed,
844
+ failed=failed,
845
+ scripts_used=scripts_used,
846
+ artifact_references=artifact_references,
847
+ failure_attribution=failure_attribution,
848
+ returncode=returncode,
849
+ stdout=stdout,
850
+ stderr=stderr,
851
+ )
852
+ save_report(base_dir, treatment_name, rep, report)
853
+
854
+ experiment_logger.add_result(
855
+ treatment_name,
856
+ TreatmentResult(
857
+ name=treatment_name,
858
+ passed=len(failed) == 0,
859
+ checks_passed=passed,
860
+ checks_failed=failed,
861
+ events_summary={
862
+ "num_turns": events.get("num_turns"),
863
+ "duration_seconds": events.get("duration_seconds"),
864
+ "tool_calls": len(events.get("tool_calls", [])),
865
+ "input_tokens": events.get("input_tokens"),
866
+ "output_tokens": events.get("output_tokens"),
867
+ "cache_read_input_tokens": events.get("cache_read_input_tokens"),
868
+ "cache_creation_input_tokens": events.get("cache_creation_input_tokens"),
869
+ "total_tokens": events.get("total_tokens"),
870
+ "total_cost_usd": events.get("total_cost_usd"),
871
+ "model_usage": events.get("model_usage", {}),
872
+ "skills_invoked": events.get("skills_invoked", []),
873
+ "scripts_used": scripts_used,
874
+ "profile": events.get("profile"),
875
+ "skill_sources": events.get("skill_sources", []),
876
+ "eval_manifest": events.get("eval_manifest"),
877
+ "interaction": events.get("interaction", {}),
878
+ "artifact_references": artifact_references,
879
+ "failure_attribution": failure_attribution,
880
+ },
881
+ run_id=run_id,
882
+ ),
883
+ )
884
+
885
+ return _record
886
+
887
+
888
+ def _build_report_payload(
889
+ *,
890
+ treatment_name: str,
891
+ rep: int,
892
+ run_id: str,
893
+ events: dict[str, Any],
894
+ passed: list[str],
895
+ failed: list[str],
896
+ scripts_used: list[str],
897
+ artifact_references: dict[str, str],
898
+ failure_attribution: list[dict[str, str]],
899
+ returncode: int | None = None,
900
+ stdout: str | None = None,
901
+ stderr: str | None = None,
902
+ ) -> dict[str, Any]:
903
+ sample_quality = infer_sample_quality(
904
+ events=events,
905
+ checks_failed=failed,
906
+ failure_attribution=failure_attribution,
907
+ stdout=stdout,
908
+ stderr=stderr,
909
+ returncode=returncode,
910
+ ).to_dict()
911
+
912
+ return {
913
+ "name": treatment_name,
914
+ "rep": rep,
915
+ "passed": len(failed) == 0,
916
+ "run_id": run_id,
917
+ "checks_passed": passed,
918
+ "checks_failed": failed,
919
+ "sample_quality": sample_quality,
920
+ "events_summary": {
921
+ "duration_seconds": events.get("duration_seconds"),
922
+ "num_turns": events.get("num_turns"),
923
+ "tool_calls": len(events.get("tool_calls", [])),
924
+ "input_tokens": events.get("input_tokens"),
925
+ "output_tokens": events.get("output_tokens"),
926
+ "cache_read_input_tokens": events.get("cache_read_input_tokens"),
927
+ "cache_creation_input_tokens": events.get("cache_creation_input_tokens"),
928
+ "total_tokens": events.get("total_tokens"),
929
+ "total_cost_usd": events.get("total_cost_usd"),
930
+ "model_usage": events.get("model_usage", {}),
931
+ "files_created": events.get("files_created", []),
932
+ "skills_invoked": events.get("skills_invoked", []),
933
+ "scripts_used": scripts_used,
934
+ "profile": events.get("profile"),
935
+ "skill_sources": events.get("skill_sources", []),
936
+ "eval_manifest": events.get("eval_manifest"),
937
+ "interaction": events.get("interaction", {}),
938
+ "artifact_references": artifact_references,
939
+ "failure_attribution": failure_attribution,
940
+ },
941
+ "timestamp": datetime.now().isoformat(),
942
+ }
943
+
944
+
945
+ # Fixture bundle accessor
946
+ _current_fixtures: SimpleNamespace | None = None
947
+
948
+
949
+ def get_fixtures() -> SimpleNamespace:
950
+ """Get the current test's fixtures bundle."""
951
+ if _current_fixtures is None:
952
+ raise RuntimeError("get_fixtures() called outside of test context")
953
+ return _current_fixtures
954
+
955
+
956
+ @pytest.fixture(scope="function", autouse=True)
957
+ def fixtures(
958
+ verify_environment,
959
+ test_dir,
960
+ setup_test_context,
961
+ run_claude,
962
+ record_result,
963
+ request,
964
+ ):
965
+ """Bundle test fixtures and make them accessible via get_fixtures()."""
966
+ global _current_fixtures
967
+ _current_fixtures = SimpleNamespace(
968
+ test_dir=test_dir,
969
+ setup_test_context=setup_test_context,
970
+ run_claude=run_claude,
971
+ record_result=record_result,
972
+ request_config=request.config,
973
+ )
974
+ yield _current_fixtures
975
+ _current_fixtures = None
976
+
977
+
978
+ # =============================================================================
979
+ # FIXTURE HELPERS
980
+ # =============================================================================
981
+
982
+
983
+ def _get_treatment_name(node) -> str:
984
+ """Extract treatment name from pytest node."""
985
+ nodeid = node.nodeid
986
+ if "[" in nodeid:
987
+ return nodeid.split("[")[1].rstrip("]")
988
+ return nodeid.split("::")[-1]
989
+
990
+
991
+ def _filter_scripts(scripts_dir: Path, script_filter: str) -> Path | None:
992
+ """Filter scripts by extension and return a temp dir with filtered scripts."""
993
+ if not scripts_dir or not scripts_dir.exists():
994
+ return None
995
+
996
+ if script_filter is None or script_filter == "all":
997
+ return scripts_dir
998
+
999
+ extensions = SCRIPT_EXTENSIONS.get(script_filter)
1000
+ if extensions is None:
1001
+ return scripts_dir
1002
+
1003
+ temp_dir = Path(tempfile.mkdtemp(prefix="scripts_"))
1004
+ copied_any = False
1005
+
1006
+ for script in scripts_dir.iterdir():
1007
+ if script.is_file() and script.suffix in extensions:
1008
+ shutil.copy2(script, temp_dir / script.name)
1009
+ copied_any = True
1010
+
1011
+ if not copied_any:
1012
+ shutil.rmtree(temp_dir)
1013
+ return None
1014
+
1015
+ return temp_dir
1016
+
1017
+
1018
+ def _build_docker_image_with_lock(environment_dir: Path) -> str | None:
1019
+ """Build Docker image with file locking to prevent race conditions."""
1020
+ if not environment_dir or not (environment_dir / "Dockerfile").exists():
1021
+ return None
1022
+
1023
+ with file_lock(DOCKER_BUILD_LOCK):
1024
+ result = run_shell("docker.sh", "build", str(environment_dir), timeout=300, check=False)
1025
+ if result.returncode == 0:
1026
+ return result.stdout.strip()
1027
+ return None
1028
+
1029
+
1030
+ @contextmanager
1031
+ def file_lock(path: Path):
1032
+ """Cross-platform exclusive file lock for pytest-xdist coordination."""
1033
+ path.parent.mkdir(parents=True, exist_ok=True)
1034
+ with open(path, "a+b") as lock_file:
1035
+ if os.name == "nt":
1036
+ import msvcrt
1037
+
1038
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
1039
+ try:
1040
+ yield
1041
+ finally:
1042
+ lock_file.seek(0)
1043
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
1044
+ else:
1045
+ import fcntl
1046
+
1047
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
1048
+ try:
1049
+ yield
1050
+ finally:
1051
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
1052
+
1053
+
1054
+ def _discover_skill_scripts() -> list[str]:
1055
+ """Dynamically discover all script files from skills directories."""
1056
+ scripts = set()
1057
+ skills_dir = PROJECT_ROOT / "skills"
1058
+
1059
+ if not skills_dir.exists():
1060
+ return []
1061
+
1062
+ for scripts_dir in skills_dir.rglob("scripts"):
1063
+ if scripts_dir.is_dir():
1064
+ for script in scripts_dir.iterdir():
1065
+ if script.is_file() and script.suffix in {".py", ".ts", ".js"}:
1066
+ scripts.add(script.name)
1067
+
1068
+ return sorted(scripts)
1069
+
1070
+
1071
+ def _get_known_scripts() -> list[str]:
1072
+ """Get known scripts, discovering them on first call."""
1073
+ global _KNOWN_SCRIPTS
1074
+ if _KNOWN_SCRIPTS is None:
1075
+ _KNOWN_SCRIPTS = _discover_skill_scripts()
1076
+ return _KNOWN_SCRIPTS
1077
+
1078
+
1079
+ def _extract_scripts_used(events: dict) -> list[str]:
1080
+ """Extract which skill scripts were used from events."""
1081
+ commands = " ".join(events.get("commands_run", [])).lower()
1082
+ files_read = " ".join(events.get("files_read", [])).lower()
1083
+ all_activity = commands + " " + files_read
1084
+
1085
+ return [s for s in _get_known_scripts() if s.lower() in all_activity]
1086
+
1087
+
1088
+ def _save_artifacts(base_dir: Path, treatment_name: str, rep: int, test_dir: Path):
1089
+ """Save Claude's generated files as artifacts."""
1090
+ artifacts_dir = base_dir / "artifacts" / f"{treatment_name.lower()}_rep{rep}"
1091
+ claude_dir = artifacts_dir / "claude"
1092
+ execution_dir = artifacts_dir / "execution"
1093
+ claude_dir.mkdir(parents=True, exist_ok=True)
1094
+ execution_dir.mkdir(parents=True, exist_ok=True)
1095
+
1096
+ from scaffold.python.utils import TEST_CONTEXT_FILE, TEST_RESULTS_FILE
1097
+
1098
+ exclude_dirs = {
1099
+ ".claude",
1100
+ ".git",
1101
+ "node_modules",
1102
+ "__pycache__",
1103
+ "scaffold",
1104
+ "validation",
1105
+ "data",
1106
+ }
1107
+ exclude_files = {
1108
+ "CLAUDE.md",
1109
+ "Dockerfile",
1110
+ "requirements.txt",
1111
+ "package.json",
1112
+ "package-lock.json",
1113
+ "tsconfig.json",
1114
+ TEST_CONTEXT_FILE,
1115
+ TEST_RESULTS_FILE,
1116
+ }
1117
+
1118
+ claude_files = []
1119
+ for item in test_dir.rglob("*"):
1120
+ if not item.is_file():
1121
+ continue
1122
+ if item.name.startswith("."):
1123
+ continue
1124
+ if item.name in exclude_files:
1125
+ continue
1126
+ if any(excl in item.parts for excl in exclude_dirs):
1127
+ continue
1128
+ try:
1129
+ rel_path = item.relative_to(test_dir)
1130
+ dest = claude_dir / rel_path
1131
+ dest.parent.mkdir(parents=True, exist_ok=True)
1132
+ shutil.copy(item, dest)
1133
+ claude_files.append(item)
1134
+ except Exception as e:
1135
+ import sys
1136
+
1137
+ print(f"Warning: failed to save artifact {item.name}: {e}", file=sys.stderr)
1138
+
1139
+ for py_file in claude_files:
1140
+ if py_file.suffix == ".py" and py_file.parent == test_dir:
1141
+ try:
1142
+ success, output = run_python_in_docker(test_dir, py_file.name, timeout=300)
1143
+ status = "success" if success else "error"
1144
+ output_file = execution_dir / f"{py_file.stem}_{status}.txt"
1145
+ output_file.write_text(strip_ansi(output))
1146
+ except Exception as e:
1147
+ error_file = execution_dir / f"{py_file.stem}_error.txt"
1148
+ error_file.write_text(str(e))