@prismatic-io/lux 0.0.2-preview.3 → 0.0.2-preview.30

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 (717) hide show
  1. package/lib/answerers/claude-code/index.d.ts +15 -3
  2. package/lib/answerers/claude-code/index.d.ts.map +1 -1
  3. package/lib/answerers/claude-code/index.js +6 -5
  4. package/lib/answerers/claude-code/index.js.map +1 -1
  5. package/lib/answerers/persona/index.d.ts +44 -5
  6. package/lib/answerers/persona/index.d.ts.map +1 -1
  7. package/lib/answerers/persona/index.js +60 -34
  8. package/lib/answerers/persona/index.js.map +1 -1
  9. package/lib/answerers/terminal/index.d.ts +9 -3
  10. package/lib/answerers/terminal/index.d.ts.map +1 -1
  11. package/lib/answerers/terminal/index.js +7 -5
  12. package/lib/answerers/terminal/index.js.map +1 -1
  13. package/lib/assertions/authoring.d.ts +2 -2
  14. package/lib/assertions/authoring.d.ts.map +1 -1
  15. package/lib/assertions/authoring.js.map +1 -1
  16. package/lib/assertions/core/command-exits-zero.d.ts +3 -1
  17. package/lib/assertions/core/command-exits-zero.d.ts.map +1 -1
  18. package/lib/assertions/core/command-exits-zero.js +50 -7
  19. package/lib/assertions/core/command-exits-zero.js.map +1 -1
  20. package/lib/assertions/core/event-checks.d.ts +3 -0
  21. package/lib/assertions/core/event-checks.d.ts.map +1 -1
  22. package/lib/assertions/core/event-checks.js +49 -3
  23. package/lib/assertions/core/event-checks.js.map +1 -1
  24. package/lib/assertions/core/helpers.d.ts +2 -7
  25. package/lib/assertions/core/helpers.d.ts.map +1 -1
  26. package/lib/assertions/core/helpers.js +2 -6
  27. package/lib/assertions/core/helpers.js.map +1 -1
  28. package/lib/assertions/core/index.d.ts.map +1 -1
  29. package/lib/assertions/core/index.js +2 -1
  30. package/lib/assertions/core/index.js.map +1 -1
  31. package/lib/assertions/core/resource-checks.d.ts +19 -0
  32. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  33. package/lib/assertions/core/resource-checks.js +35 -5
  34. package/lib/assertions/core/resource-checks.js.map +1 -1
  35. package/lib/assertions/rubric/index.d.ts +26 -3
  36. package/lib/assertions/rubric/index.d.ts.map +1 -1
  37. package/lib/assertions/rubric/index.js +59 -13
  38. package/lib/assertions/rubric/index.js.map +1 -1
  39. package/lib/assertions/rubric/internal.d.ts +30 -5
  40. package/lib/assertions/rubric/internal.d.ts.map +1 -1
  41. package/lib/assertions/rubric/internal.js +181 -28
  42. package/lib/assertions/rubric/internal.js.map +1 -1
  43. package/lib/authoring.d.ts +36 -2
  44. package/lib/authoring.d.ts.map +1 -1
  45. package/lib/authoring.js +36 -1
  46. package/lib/authoring.js.map +1 -1
  47. package/lib/cli/bin.js +2 -13
  48. package/lib/cli/bin.js.map +1 -1
  49. package/lib/cli/command-runtime.d.ts +3 -3
  50. package/lib/cli/command-runtime.d.ts.map +1 -1
  51. package/lib/cli/command-runtime.js +17 -6
  52. package/lib/cli/command-runtime.js.map +1 -1
  53. package/lib/cli/doctor.d.ts +55 -0
  54. package/lib/cli/doctor.d.ts.map +1 -0
  55. package/lib/cli/doctor.js +14 -0
  56. package/lib/cli/doctor.js.map +1 -0
  57. package/lib/cli/harness-plan.d.ts +26 -0
  58. package/lib/cli/harness-plan.d.ts.map +1 -0
  59. package/lib/cli/harness-plan.js +75 -0
  60. package/lib/cli/harness-plan.js.map +1 -0
  61. package/lib/cli/init-templates.d.ts +11 -0
  62. package/lib/cli/init-templates.d.ts.map +1 -0
  63. package/lib/cli/init-templates.js +215 -0
  64. package/lib/cli/init-templates.js.map +1 -0
  65. package/lib/cli/init.d.ts +10 -0
  66. package/lib/cli/init.d.ts.map +1 -1
  67. package/lib/cli/init.js +34 -173
  68. package/lib/cli/init.js.map +1 -1
  69. package/lib/cli/output-schemas.d.ts +1406 -0
  70. package/lib/cli/output-schemas.d.ts.map +1 -0
  71. package/lib/cli/output-schemas.js +91 -0
  72. package/lib/cli/output-schemas.js.map +1 -0
  73. package/lib/cli/program.d.ts +140 -12
  74. package/lib/cli/program.d.ts.map +1 -1
  75. package/lib/cli/program.js +778 -646
  76. package/lib/cli/program.js.map +1 -1
  77. package/lib/cli/render/reporter.d.ts.map +1 -1
  78. package/lib/cli/render/reporter.js +40 -7
  79. package/lib/cli/render/reporter.js.map +1 -1
  80. package/lib/cli/run-options.d.ts +20 -4
  81. package/lib/cli/run-options.d.ts.map +1 -1
  82. package/lib/cli/run-options.js +100 -19
  83. package/lib/cli/run-options.js.map +1 -1
  84. package/lib/cli/view.d.ts +15 -0
  85. package/lib/cli/view.d.ts.map +1 -0
  86. package/lib/cli/view.js +168 -0
  87. package/lib/cli/view.js.map +1 -0
  88. package/lib/container/driver.d.ts +27 -0
  89. package/lib/container/driver.d.ts.map +1 -0
  90. package/lib/container/driver.js +197 -0
  91. package/lib/container/driver.js.map +1 -0
  92. package/lib/container/protocol.d.ts +12 -0
  93. package/lib/container/protocol.d.ts.map +1 -0
  94. package/lib/container/protocol.js +38 -0
  95. package/lib/container/protocol.js.map +1 -0
  96. package/lib/container/resources.d.ts +12 -0
  97. package/lib/container/resources.d.ts.map +1 -0
  98. package/lib/container/resources.js +133 -0
  99. package/lib/container/resources.js.map +1 -0
  100. package/lib/container/worker.d.ts +2 -0
  101. package/lib/container/worker.d.ts.map +1 -0
  102. package/lib/container/worker.js +85 -0
  103. package/lib/container/worker.js.map +1 -0
  104. package/lib/core/action-events.d.ts +29 -0
  105. package/lib/core/action-events.d.ts.map +1 -0
  106. package/lib/core/action-events.js +51 -0
  107. package/lib/core/action-events.js.map +1 -0
  108. package/lib/core/agent-activity.d.ts +49 -0
  109. package/lib/core/agent-activity.d.ts.map +1 -0
  110. package/lib/core/agent-activity.js +206 -0
  111. package/lib/core/agent-activity.js.map +1 -0
  112. package/lib/core/agent-environment.d.ts +5 -0
  113. package/lib/core/agent-environment.d.ts.map +1 -0
  114. package/lib/core/agent-environment.js +47 -0
  115. package/lib/core/agent-environment.js.map +1 -0
  116. package/lib/core/agent-events.d.ts +26 -0
  117. package/lib/core/agent-events.d.ts.map +1 -0
  118. package/lib/core/agent-events.js +13 -0
  119. package/lib/core/agent-events.js.map +1 -0
  120. package/lib/core/annotation.d.ts +6 -2
  121. package/lib/core/annotation.d.ts.map +1 -1
  122. package/lib/core/annotation.js +2 -1
  123. package/lib/core/annotation.js.map +1 -1
  124. package/lib/core/answerer.d.ts +12 -0
  125. package/lib/core/answerer.d.ts.map +1 -1
  126. package/lib/core/answerer.js +2 -0
  127. package/lib/core/answerer.js.map +1 -1
  128. package/lib/core/artifact-evidence.d.ts +8 -0
  129. package/lib/core/artifact-evidence.d.ts.map +1 -1
  130. package/lib/core/artifact-evidence.js +36 -4
  131. package/lib/core/artifact-evidence.js.map +1 -1
  132. package/lib/core/assertion.d.ts +15 -0
  133. package/lib/core/assertion.d.ts.map +1 -1
  134. package/lib/core/assertion.js +16 -0
  135. package/lib/core/assertion.js.map +1 -1
  136. package/lib/core/case.d.ts +106 -2
  137. package/lib/core/case.d.ts.map +1 -1
  138. package/lib/core/case.js +16 -2
  139. package/lib/core/case.js.map +1 -1
  140. package/lib/core/command-verification.d.ts +10 -0
  141. package/lib/core/command-verification.d.ts.map +1 -0
  142. package/lib/core/command-verification.js +66 -0
  143. package/lib/core/command-verification.js.map +1 -0
  144. package/lib/core/driver-execution.d.ts +83 -0
  145. package/lib/core/driver-execution.d.ts.map +1 -0
  146. package/lib/core/driver-execution.js +72 -0
  147. package/lib/core/driver-execution.js.map +1 -0
  148. package/lib/core/driver-metrics.d.ts +98 -0
  149. package/lib/core/driver-metrics.d.ts.map +1 -0
  150. package/lib/core/driver-metrics.js +36 -0
  151. package/lib/core/driver-metrics.js.map +1 -0
  152. package/lib/core/driver.d.ts +64 -0
  153. package/lib/core/driver.d.ts.map +1 -1
  154. package/lib/core/driver.js +28 -0
  155. package/lib/core/driver.js.map +1 -1
  156. package/lib/core/duration.d.ts +5 -0
  157. package/lib/core/duration.d.ts.map +1 -0
  158. package/lib/core/duration.js +49 -0
  159. package/lib/core/duration.js.map +1 -0
  160. package/lib/core/environment.d.ts +18 -0
  161. package/lib/core/environment.d.ts.map +1 -0
  162. package/lib/core/environment.js +21 -0
  163. package/lib/core/environment.js.map +1 -0
  164. package/lib/core/evaluation-clusters.d.ts +17 -0
  165. package/lib/core/evaluation-clusters.d.ts.map +1 -0
  166. package/lib/core/evaluation-clusters.js +23 -0
  167. package/lib/core/evaluation-clusters.js.map +1 -0
  168. package/lib/core/events.d.ts +36 -0
  169. package/lib/core/events.d.ts.map +1 -1
  170. package/lib/core/events.js +12 -2
  171. package/lib/core/events.js.map +1 -1
  172. package/lib/core/experiment.d.ts +32 -2
  173. package/lib/core/experiment.d.ts.map +1 -1
  174. package/lib/core/experiment.js +35 -18
  175. package/lib/core/experiment.js.map +1 -1
  176. package/lib/core/file-tree.d.ts +7 -0
  177. package/lib/core/file-tree.d.ts.map +1 -0
  178. package/lib/core/file-tree.js +35 -0
  179. package/lib/core/file-tree.js.map +1 -0
  180. package/lib/core/global-id.d.ts +6 -0
  181. package/lib/core/global-id.d.ts.map +1 -0
  182. package/lib/core/global-id.js +17 -0
  183. package/lib/core/global-id.js.map +1 -0
  184. package/lib/core/harness-catalog.d.ts +35 -0
  185. package/lib/core/harness-catalog.d.ts.map +1 -0
  186. package/lib/core/harness-catalog.js +45 -0
  187. package/lib/core/harness-catalog.js.map +1 -0
  188. package/lib/core/harness-catalogs/antigravity.d.ts +11 -0
  189. package/lib/core/harness-catalogs/antigravity.d.ts.map +1 -0
  190. package/lib/core/harness-catalogs/antigravity.js +23 -0
  191. package/lib/core/harness-catalogs/antigravity.js.map +1 -0
  192. package/lib/core/harness-catalogs/claude-code.d.ts +12 -0
  193. package/lib/core/harness-catalogs/claude-code.d.ts.map +1 -0
  194. package/lib/core/harness-catalogs/claude-code.js +24 -0
  195. package/lib/core/harness-catalogs/claude-code.js.map +1 -0
  196. package/lib/core/harness-catalogs/codex.d.ts +11 -0
  197. package/lib/core/harness-catalogs/codex.d.ts.map +1 -0
  198. package/lib/core/harness-catalogs/codex.js +26 -0
  199. package/lib/core/harness-catalogs/codex.js.map +1 -0
  200. package/lib/core/harness-catalogs/copilot.d.ts +29 -0
  201. package/lib/core/harness-catalogs/copilot.d.ts.map +1 -0
  202. package/lib/core/harness-catalogs/copilot.js +45 -0
  203. package/lib/core/harness-catalogs/copilot.js.map +1 -0
  204. package/lib/core/harness-catalogs/cursor.d.ts +2 -0
  205. package/lib/core/harness-catalogs/cursor.d.ts.map +1 -0
  206. package/lib/core/harness-catalogs/cursor.js +13 -0
  207. package/lib/core/harness-catalogs/cursor.js.map +1 -0
  208. package/lib/core/harness-catalogs/grok-build.d.ts +7 -0
  209. package/lib/core/harness-catalogs/grok-build.d.ts.map +1 -0
  210. package/lib/core/harness-catalogs/grok-build.js +18 -0
  211. package/lib/core/harness-catalogs/grok-build.js.map +1 -0
  212. package/lib/core/harness-catalogs/index.d.ts +73 -0
  213. package/lib/core/harness-catalogs/index.d.ts.map +1 -0
  214. package/lib/core/harness-catalogs/index.js +17 -0
  215. package/lib/core/harness-catalogs/index.js.map +1 -0
  216. package/lib/core/harness-matrix.d.ts +154 -0
  217. package/lib/core/harness-matrix.d.ts.map +1 -0
  218. package/lib/core/harness-matrix.js +204 -0
  219. package/lib/core/harness-matrix.js.map +1 -0
  220. package/lib/core/index.d.ts +12 -0
  221. package/lib/core/index.d.ts.map +1 -1
  222. package/lib/core/index.js +12 -0
  223. package/lib/core/index.js.map +1 -1
  224. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  225. package/lib/core/lifecycle-fixtures.js +4 -2
  226. package/lib/core/lifecycle-fixtures.js.map +1 -1
  227. package/lib/core/model-cli.d.ts +9 -11
  228. package/lib/core/model-cli.d.ts.map +1 -1
  229. package/lib/core/model-cli.js +31 -19
  230. package/lib/core/model-cli.js.map +1 -1
  231. package/lib/core/platform-process.d.ts +6 -1
  232. package/lib/core/platform-process.d.ts.map +1 -1
  233. package/lib/core/platform-process.js +49 -11
  234. package/lib/core/platform-process.js.map +1 -1
  235. package/lib/core/run.d.ts +292 -1
  236. package/lib/core/run.d.ts.map +1 -1
  237. package/lib/core/run.js +27 -0
  238. package/lib/core/run.js.map +1 -1
  239. package/lib/core/secure-copy.d.ts.map +1 -0
  240. package/lib/core/secure-copy.js.map +1 -0
  241. package/lib/core/usage.d.ts +5 -0
  242. package/lib/core/usage.d.ts.map +1 -1
  243. package/lib/core/usage.js +3 -0
  244. package/lib/core/usage.js.map +1 -1
  245. package/lib/core/wire.d.ts +12 -0
  246. package/lib/core/wire.d.ts.map +1 -1
  247. package/lib/core/wire.js +5 -1
  248. package/lib/core/wire.js.map +1 -1
  249. package/lib/drivers/antigravity/artifacts.d.ts +2 -0
  250. package/lib/drivers/antigravity/artifacts.d.ts.map +1 -0
  251. package/lib/drivers/antigravity/artifacts.js +33 -0
  252. package/lib/drivers/antigravity/artifacts.js.map +1 -0
  253. package/lib/drivers/antigravity/config.d.ts +88 -0
  254. package/lib/drivers/antigravity/config.d.ts.map +1 -0
  255. package/lib/drivers/antigravity/config.js +87 -0
  256. package/lib/drivers/antigravity/config.js.map +1 -0
  257. package/lib/drivers/antigravity/events.d.ts +4 -0
  258. package/lib/drivers/antigravity/events.d.ts.map +1 -0
  259. package/lib/drivers/antigravity/events.js +241 -0
  260. package/lib/drivers/antigravity/events.js.map +1 -0
  261. package/lib/drivers/antigravity/index.d.ts +114 -0
  262. package/lib/drivers/antigravity/index.d.ts.map +1 -0
  263. package/lib/drivers/antigravity/index.js +113 -0
  264. package/lib/drivers/antigravity/index.js.map +1 -0
  265. package/lib/drivers/antigravity/runtime.d.ts +7 -0
  266. package/lib/drivers/antigravity/runtime.d.ts.map +1 -0
  267. package/lib/drivers/antigravity/runtime.js +83 -0
  268. package/lib/drivers/antigravity/runtime.js.map +1 -0
  269. package/lib/drivers/claude-code/deferred-process.d.ts +17 -0
  270. package/lib/drivers/claude-code/deferred-process.d.ts.map +1 -0
  271. package/lib/drivers/claude-code/deferred-process.js +108 -0
  272. package/lib/drivers/claude-code/deferred-process.js.map +1 -0
  273. package/lib/drivers/claude-code/index.d.ts +180 -10
  274. package/lib/drivers/claude-code/index.d.ts.map +1 -1
  275. package/lib/drivers/claude-code/index.js +186 -252
  276. package/lib/drivers/claude-code/index.js.map +1 -1
  277. package/lib/drivers/claude-code/isolation.d.ts +8 -0
  278. package/lib/drivers/claude-code/isolation.d.ts.map +1 -0
  279. package/lib/drivers/claude-code/isolation.js +199 -0
  280. package/lib/drivers/claude-code/isolation.js.map +1 -0
  281. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
  282. package/lib/drivers/claude-code/parse-events.js +73 -8
  283. package/lib/drivers/claude-code/parse-events.js.map +1 -1
  284. package/lib/drivers/codex/app-events.d.ts +9 -2
  285. package/lib/drivers/codex/app-events.d.ts.map +1 -1
  286. package/lib/drivers/codex/app-events.js +104 -12
  287. package/lib/drivers/codex/app-events.js.map +1 -1
  288. package/lib/drivers/codex/config.d.ts +66 -5
  289. package/lib/drivers/codex/config.d.ts.map +1 -1
  290. package/lib/drivers/codex/config.js +39 -8
  291. package/lib/drivers/codex/config.js.map +1 -1
  292. package/lib/drivers/codex/exec-events.d.ts +4 -0
  293. package/lib/drivers/codex/exec-events.d.ts.map +1 -0
  294. package/lib/drivers/codex/exec-events.js +230 -0
  295. package/lib/drivers/codex/exec-events.js.map +1 -0
  296. package/lib/drivers/codex/index.d.ts +138 -4
  297. package/lib/drivers/codex/index.d.ts.map +1 -1
  298. package/lib/drivers/codex/index.js +112 -276
  299. package/lib/drivers/codex/index.js.map +1 -1
  300. package/lib/drivers/copilot/config.d.ts +84 -0
  301. package/lib/drivers/copilot/config.d.ts.map +1 -0
  302. package/lib/drivers/copilot/config.js +57 -0
  303. package/lib/drivers/copilot/config.js.map +1 -0
  304. package/lib/drivers/copilot/index.d.ts +107 -0
  305. package/lib/drivers/copilot/index.d.ts.map +1 -0
  306. package/lib/drivers/copilot/index.js +268 -0
  307. package/lib/drivers/copilot/index.js.map +1 -0
  308. package/lib/drivers/cursor/config.d.ts +28 -0
  309. package/lib/drivers/cursor/config.d.ts.map +1 -0
  310. package/lib/drivers/cursor/config.js +28 -0
  311. package/lib/drivers/cursor/config.js.map +1 -0
  312. package/lib/drivers/cursor/index.d.ts +44 -0
  313. package/lib/drivers/cursor/index.d.ts.map +1 -0
  314. package/lib/drivers/cursor/index.js +286 -0
  315. package/lib/drivers/cursor/index.js.map +1 -0
  316. package/lib/drivers/cursor/interaction.d.ts +6 -0
  317. package/lib/drivers/cursor/interaction.d.ts.map +1 -0
  318. package/lib/drivers/cursor/interaction.js +55 -0
  319. package/lib/drivers/cursor/interaction.js.map +1 -0
  320. package/lib/drivers/grok-build/config.d.ts +68 -0
  321. package/lib/drivers/grok-build/config.d.ts.map +1 -0
  322. package/lib/drivers/grok-build/config.js +40 -0
  323. package/lib/drivers/grok-build/config.js.map +1 -0
  324. package/lib/drivers/grok-build/index.d.ts +87 -0
  325. package/lib/drivers/grok-build/index.d.ts.map +1 -0
  326. package/lib/drivers/grok-build/index.js +293 -0
  327. package/lib/drivers/grok-build/index.js.map +1 -0
  328. package/lib/drivers/mcp/index.d.ts +37 -4
  329. package/lib/drivers/mcp/index.d.ts.map +1 -1
  330. package/lib/drivers/mcp/index.js +25 -11
  331. package/lib/drivers/mcp/index.js.map +1 -1
  332. package/lib/drivers/shared/acp-events.d.ts +15 -0
  333. package/lib/drivers/shared/acp-events.d.ts.map +1 -0
  334. package/lib/drivers/shared/acp-events.js +83 -0
  335. package/lib/drivers/shared/acp-events.js.map +1 -0
  336. package/lib/drivers/shared/acp-interaction.d.ts +7 -0
  337. package/lib/drivers/shared/acp-interaction.d.ts.map +1 -0
  338. package/lib/drivers/shared/acp-interaction.js +20 -0
  339. package/lib/drivers/shared/acp-interaction.js.map +1 -0
  340. package/lib/drivers/shared/acp-session.d.ts +4 -0
  341. package/lib/drivers/shared/acp-session.d.ts.map +1 -0
  342. package/lib/drivers/shared/acp-session.js +23 -0
  343. package/lib/drivers/shared/acp-session.js.map +1 -0
  344. package/lib/drivers/shared/acp-transport.d.ts +40 -0
  345. package/lib/drivers/shared/acp-transport.d.ts.map +1 -0
  346. package/lib/drivers/shared/acp-transport.js +152 -0
  347. package/lib/drivers/shared/acp-transport.js.map +1 -0
  348. package/lib/drivers/shared/artifacts.d.ts.map +1 -1
  349. package/lib/drivers/shared/artifacts.js +3 -2
  350. package/lib/drivers/shared/artifacts.js.map +1 -1
  351. package/lib/drivers/shared/experiment-behavior.d.ts +13 -0
  352. package/lib/drivers/shared/experiment-behavior.d.ts.map +1 -0
  353. package/lib/drivers/shared/experiment-behavior.js +197 -0
  354. package/lib/drivers/shared/experiment-behavior.js.map +1 -0
  355. package/lib/drivers/shared/experiment-policy.d.ts +4 -0
  356. package/lib/drivers/shared/experiment-policy.d.ts.map +1 -0
  357. package/lib/drivers/shared/experiment-policy.js +143 -0
  358. package/lib/drivers/shared/experiment-policy.js.map +1 -0
  359. package/lib/drivers/subprocess/index.d.ts +39 -4
  360. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  361. package/lib/drivers/subprocess/index.js +56 -12
  362. package/lib/drivers/subprocess/index.js.map +1 -1
  363. package/lib/index.d.ts +22 -8
  364. package/lib/index.d.ts.map +1 -1
  365. package/lib/index.js +20 -6
  366. package/lib/index.js.map +1 -1
  367. package/lib/optimization/reflective.d.ts +3 -1
  368. package/lib/optimization/reflective.d.ts.map +1 -1
  369. package/lib/optimization/reflective.js +3 -2
  370. package/lib/optimization/reflective.js.map +1 -1
  371. package/lib/orchestrator/annotation-loader.d.ts +6 -2
  372. package/lib/orchestrator/annotation-loader.d.ts.map +1 -1
  373. package/lib/orchestrator/annotation-loader.js +8 -0
  374. package/lib/orchestrator/annotation-loader.js.map +1 -1
  375. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  376. package/lib/orchestrator/annotation-store.js +16 -2
  377. package/lib/orchestrator/annotation-store.js.map +1 -1
  378. package/lib/orchestrator/artifact-origin.d.ts +3 -0
  379. package/lib/orchestrator/artifact-origin.d.ts.map +1 -0
  380. package/lib/orchestrator/artifact-origin.js +40 -0
  381. package/lib/orchestrator/artifact-origin.js.map +1 -0
  382. package/lib/orchestrator/campaign-lifecycle.d.ts +1 -0
  383. package/lib/orchestrator/campaign-lifecycle.d.ts.map +1 -1
  384. package/lib/orchestrator/campaign-lifecycle.js +4 -1
  385. package/lib/orchestrator/campaign-lifecycle.js.map +1 -1
  386. package/lib/orchestrator/candidates.d.ts.map +1 -1
  387. package/lib/orchestrator/candidates.js +3 -2
  388. package/lib/orchestrator/candidates.js.map +1 -1
  389. package/lib/orchestrator/compare.d.ts +17 -1
  390. package/lib/orchestrator/compare.d.ts.map +1 -1
  391. package/lib/orchestrator/compare.js +25 -1
  392. package/lib/orchestrator/compare.js.map +1 -1
  393. package/lib/orchestrator/comparison-identity.d.ts.map +1 -1
  394. package/lib/orchestrator/comparison-identity.js +2 -0
  395. package/lib/orchestrator/comparison-identity.js.map +1 -1
  396. package/lib/orchestrator/config.d.ts +177 -1
  397. package/lib/orchestrator/config.d.ts.map +1 -1
  398. package/lib/orchestrator/config.js +18 -4
  399. package/lib/orchestrator/config.js.map +1 -1
  400. package/lib/orchestrator/discover.d.ts +1 -0
  401. package/lib/orchestrator/discover.d.ts.map +1 -1
  402. package/lib/orchestrator/discover.js +7 -1
  403. package/lib/orchestrator/discover.js.map +1 -1
  404. package/lib/orchestrator/doctor.d.ts +1 -0
  405. package/lib/orchestrator/doctor.d.ts.map +1 -1
  406. package/lib/orchestrator/doctor.js +121 -33
  407. package/lib/orchestrator/doctor.js.map +1 -1
  408. package/lib/orchestrator/driver-capabilities.d.ts +4 -0
  409. package/lib/orchestrator/driver-capabilities.d.ts.map +1 -0
  410. package/lib/orchestrator/driver-capabilities.js +13 -0
  411. package/lib/orchestrator/driver-capabilities.js.map +1 -0
  412. package/lib/orchestrator/driver-identity.d.ts +5 -0
  413. package/lib/orchestrator/driver-identity.d.ts.map +1 -0
  414. package/lib/orchestrator/driver-identity.js +51 -0
  415. package/lib/orchestrator/driver-identity.js.map +1 -0
  416. package/lib/orchestrator/experiment-context.d.ts +51 -3
  417. package/lib/orchestrator/experiment-context.d.ts.map +1 -1
  418. package/lib/orchestrator/experiment-context.js.map +1 -1
  419. package/lib/orchestrator/experiment-contracts.d.ts +30 -1
  420. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  421. package/lib/orchestrator/experiment-corpus.d.ts +2 -1
  422. package/lib/orchestrator/experiment-corpus.d.ts.map +1 -1
  423. package/lib/orchestrator/experiment-corpus.js +28 -22
  424. package/lib/orchestrator/experiment-corpus.js.map +1 -1
  425. package/lib/orchestrator/experiment-effects.d.ts +33 -0
  426. package/lib/orchestrator/experiment-effects.d.ts.map +1 -0
  427. package/lib/orchestrator/experiment-effects.js +44 -0
  428. package/lib/orchestrator/experiment-effects.js.map +1 -0
  429. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  430. package/lib/orchestrator/experiment-evaluation.js +56 -5
  431. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  432. package/lib/orchestrator/experiment-identity.d.ts +3 -0
  433. package/lib/orchestrator/experiment-identity.d.ts.map +1 -1
  434. package/lib/orchestrator/experiment-identity.js +22 -33
  435. package/lib/orchestrator/experiment-identity.js.map +1 -1
  436. package/lib/orchestrator/experiment-loader.d.ts +8 -3
  437. package/lib/orchestrator/experiment-loader.d.ts.map +1 -1
  438. package/lib/orchestrator/experiment-preflight.d.ts +3 -0
  439. package/lib/orchestrator/experiment-preflight.d.ts.map +1 -0
  440. package/lib/orchestrator/experiment-preflight.js +21 -0
  441. package/lib/orchestrator/experiment-preflight.js.map +1 -0
  442. package/lib/orchestrator/experiment-report.d.ts +90 -1
  443. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  444. package/lib/orchestrator/experiment-report.js +9 -1
  445. package/lib/orchestrator/experiment-report.js.map +1 -1
  446. package/lib/orchestrator/experiment-runs.d.ts +61 -2
  447. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  448. package/lib/orchestrator/experiment-runs.js +13 -5
  449. package/lib/orchestrator/experiment-runs.js.map +1 -1
  450. package/lib/orchestrator/experiment-runtime-identity.d.ts +0 -16
  451. package/lib/orchestrator/experiment-runtime-identity.d.ts.map +1 -1
  452. package/lib/orchestrator/experiment-runtime-identity.js +5 -220
  453. package/lib/orchestrator/experiment-runtime-identity.js.map +1 -1
  454. package/lib/orchestrator/experiment-selection.d.ts.map +1 -1
  455. package/lib/orchestrator/experiment-selection.js +52 -0
  456. package/lib/orchestrator/experiment-selection.js.map +1 -1
  457. package/lib/orchestrator/experiment.d.ts +3 -3
  458. package/lib/orchestrator/experiment.d.ts.map +1 -1
  459. package/lib/orchestrator/experiment.js +26 -21
  460. package/lib/orchestrator/experiment.js.map +1 -1
  461. package/lib/orchestrator/fixture-identity.d.ts +1 -0
  462. package/lib/orchestrator/fixture-identity.d.ts.map +1 -1
  463. package/lib/orchestrator/fixture-identity.js +6 -1
  464. package/lib/orchestrator/fixture-identity.js.map +1 -1
  465. package/lib/orchestrator/fs-read.d.ts +1 -6
  466. package/lib/orchestrator/fs-read.d.ts.map +1 -1
  467. package/lib/orchestrator/fs-read.js +2 -32
  468. package/lib/orchestrator/fs-read.js.map +1 -1
  469. package/lib/orchestrator/grader-experiment.d.ts +29 -1
  470. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  471. package/lib/orchestrator/grader-experiment.js +54 -9
  472. package/lib/orchestrator/grader-experiment.js.map +1 -1
  473. package/lib/orchestrator/grading.d.ts +2 -1
  474. package/lib/orchestrator/grading.d.ts.map +1 -1
  475. package/lib/orchestrator/grading.js +27 -8
  476. package/lib/orchestrator/grading.js.map +1 -1
  477. package/lib/orchestrator/harness-catalog-drift.d.ts +22 -0
  478. package/lib/orchestrator/harness-catalog-drift.d.ts.map +1 -0
  479. package/lib/orchestrator/harness-catalog-drift.js +90 -0
  480. package/lib/orchestrator/harness-catalog-drift.js.map +1 -0
  481. package/lib/orchestrator/harness-registry.d.ts +4 -0
  482. package/lib/orchestrator/harness-registry.d.ts.map +1 -0
  483. package/lib/orchestrator/harness-registry.js +5 -0
  484. package/lib/orchestrator/harness-registry.js.map +1 -0
  485. package/lib/orchestrator/index.d.ts +1 -1
  486. package/lib/orchestrator/index.d.ts.map +1 -1
  487. package/lib/orchestrator/index.js +1 -1
  488. package/lib/orchestrator/index.js.map +1 -1
  489. package/lib/orchestrator/list-runs.d.ts +8 -1
  490. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  491. package/lib/orchestrator/list-runs.js +77 -12
  492. package/lib/orchestrator/list-runs.js.map +1 -1
  493. package/lib/orchestrator/optimization-lifecycle.d.ts.map +1 -1
  494. package/lib/orchestrator/optimization-lifecycle.js +3 -1
  495. package/lib/orchestrator/optimization-lifecycle.js.map +1 -1
  496. package/lib/orchestrator/orchestrator.d.ts +18 -2
  497. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  498. package/lib/orchestrator/orchestrator.js +50 -168
  499. package/lib/orchestrator/orchestrator.js.map +1 -1
  500. package/lib/orchestrator/paired-effects.d.ts +34 -0
  501. package/lib/orchestrator/paired-effects.d.ts.map +1 -0
  502. package/lib/orchestrator/paired-effects.js +158 -0
  503. package/lib/orchestrator/paired-effects.js.map +1 -0
  504. package/lib/orchestrator/provenance.d.ts +5 -1
  505. package/lib/orchestrator/provenance.d.ts.map +1 -1
  506. package/lib/orchestrator/provenance.js +16 -3
  507. package/lib/orchestrator/provenance.js.map +1 -1
  508. package/lib/orchestrator/regrade.d.ts +7 -0
  509. package/lib/orchestrator/regrade.d.ts.map +1 -1
  510. package/lib/orchestrator/rubric-loader.js +1 -1
  511. package/lib/orchestrator/rubric-loader.js.map +1 -1
  512. package/lib/orchestrator/run-dir.d.ts +2 -0
  513. package/lib/orchestrator/run-dir.d.ts.map +1 -1
  514. package/lib/orchestrator/run-dir.js +1 -1
  515. package/lib/orchestrator/run-dir.js.map +1 -1
  516. package/lib/orchestrator/run-execution.d.ts +3 -0
  517. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  518. package/lib/orchestrator/run-execution.js +66 -12
  519. package/lib/orchestrator/run-execution.js.map +1 -1
  520. package/lib/orchestrator/run-lifecycle.d.ts +1 -0
  521. package/lib/orchestrator/run-lifecycle.d.ts.map +1 -1
  522. package/lib/orchestrator/run-lifecycle.js +3 -2
  523. package/lib/orchestrator/run-lifecycle.js.map +1 -1
  524. package/lib/orchestrator/run-workspace.d.ts +10 -0
  525. package/lib/orchestrator/run-workspace.d.ts.map +1 -0
  526. package/lib/orchestrator/run-workspace.js +58 -0
  527. package/lib/orchestrator/run-workspace.js.map +1 -0
  528. package/lib/orchestrator/suite-compare.d.ts +37 -0
  529. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  530. package/lib/orchestrator/suite-compare.js +27 -17
  531. package/lib/orchestrator/suite-compare.js.map +1 -1
  532. package/lib/orchestrator/suite-runs.d.ts +6 -1
  533. package/lib/orchestrator/suite-runs.d.ts.map +1 -1
  534. package/lib/orchestrator/suite-runs.js +14 -2
  535. package/lib/orchestrator/suite-runs.js.map +1 -1
  536. package/lib/orchestrator/suite-summary.d.ts +77 -0
  537. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  538. package/lib/orchestrator/suite-summary.js +61 -12
  539. package/lib/orchestrator/suite-summary.js.map +1 -1
  540. package/lib/previewer/artifacts.d.ts +25 -0
  541. package/lib/previewer/artifacts.d.ts.map +1 -0
  542. package/lib/previewer/artifacts.js +180 -0
  543. package/lib/previewer/artifacts.js.map +1 -0
  544. package/lib/previewer/server.d.ts +111 -0
  545. package/lib/previewer/server.d.ts.map +1 -0
  546. package/lib/previewer/server.js +558 -0
  547. package/lib/previewer/server.js.map +1 -0
  548. package/package.json +3 -3
  549. package/skills/lux-answerer/SKILL.md +2 -2
  550. package/src/answerers/claude-code/index.ts +6 -5
  551. package/src/answerers/persona/index.ts +75 -34
  552. package/src/answerers/terminal/index.ts +8 -12
  553. package/src/assertions/authoring.ts +2 -0
  554. package/src/assertions/core/command-exits-zero.ts +61 -7
  555. package/src/assertions/core/event-checks.ts +64 -3
  556. package/src/assertions/core/helpers.ts +3 -6
  557. package/src/assertions/core/index.ts +2 -0
  558. package/src/assertions/core/resource-checks.ts +38 -5
  559. package/src/assertions/rubric/index.ts +68 -13
  560. package/src/assertions/rubric/internal.ts +227 -27
  561. package/src/authoring.ts +345 -2
  562. package/src/cli/bin.ts +2 -12
  563. package/src/cli/command-runtime.ts +20 -9
  564. package/src/cli/doctor.ts +17 -0
  565. package/src/cli/harness-plan.ts +117 -0
  566. package/src/cli/init-templates.ts +238 -0
  567. package/src/cli/init.ts +53 -184
  568. package/src/cli/output-schemas.ts +106 -0
  569. package/src/cli/program.ts +689 -721
  570. package/src/cli/render/reporter.ts +43 -7
  571. package/src/cli/run-options.ts +132 -25
  572. package/src/cli/view.ts +129 -0
  573. package/src/container/driver.ts +211 -0
  574. package/src/container/protocol.ts +48 -0
  575. package/src/container/resources.ts +153 -0
  576. package/src/container/worker.ts +107 -0
  577. package/src/core/action-events.ts +59 -0
  578. package/src/core/agent-activity.ts +236 -0
  579. package/src/core/agent-environment.ts +64 -0
  580. package/src/core/agent-events.ts +17 -0
  581. package/src/core/annotation.ts +2 -1
  582. package/src/core/answerer.ts +9 -0
  583. package/src/core/artifact-evidence.ts +47 -4
  584. package/src/core/assertion.ts +18 -0
  585. package/src/core/case.ts +16 -2
  586. package/src/core/command-verification.ts +74 -0
  587. package/src/core/driver-execution.ts +84 -0
  588. package/src/core/driver-metrics.ts +51 -0
  589. package/src/core/driver.ts +76 -0
  590. package/src/core/duration.ts +52 -0
  591. package/src/core/environment.ts +22 -0
  592. package/src/core/evaluation-clusters.ts +35 -0
  593. package/src/core/events.ts +14 -2
  594. package/src/core/experiment.ts +64 -24
  595. package/src/core/file-tree.ts +35 -0
  596. package/src/core/global-id.ts +26 -0
  597. package/src/core/harness-catalog.ts +100 -0
  598. package/src/core/harness-catalogs/antigravity.ts +24 -0
  599. package/src/core/harness-catalogs/claude-code.ts +25 -0
  600. package/src/core/harness-catalogs/codex.ts +27 -0
  601. package/src/core/harness-catalogs/copilot.ts +46 -0
  602. package/src/core/harness-catalogs/cursor.ts +13 -0
  603. package/src/core/harness-catalogs/grok-build.ts +19 -0
  604. package/src/core/harness-catalogs/index.ts +31 -0
  605. package/src/core/harness-matrix.ts +316 -0
  606. package/src/core/index.ts +12 -0
  607. package/src/core/lifecycle-fixtures.ts +4 -2
  608. package/src/core/model-cli.ts +52 -23
  609. package/src/core/platform-process.ts +58 -10
  610. package/src/core/run.ts +31 -0
  611. package/src/core/usage.ts +8 -0
  612. package/src/core/wire.ts +5 -1
  613. package/src/drivers/antigravity/README.md +171 -0
  614. package/src/drivers/antigravity/artifacts.ts +37 -0
  615. package/src/drivers/antigravity/config.ts +92 -0
  616. package/src/drivers/antigravity/events.ts +290 -0
  617. package/src/drivers/antigravity/index.ts +132 -0
  618. package/src/drivers/antigravity/runtime.ts +94 -0
  619. package/src/drivers/claude-code/deferred-process.ts +118 -0
  620. package/src/drivers/claude-code/index.ts +216 -281
  621. package/src/drivers/claude-code/isolation.ts +228 -0
  622. package/src/drivers/claude-code/parse-events.ts +98 -12
  623. package/src/drivers/codex/app-events.ts +126 -16
  624. package/src/drivers/codex/config.ts +50 -8
  625. package/src/drivers/codex/exec-events.ts +258 -0
  626. package/src/drivers/codex/index.ts +144 -312
  627. package/src/drivers/copilot/README.md +60 -0
  628. package/src/drivers/copilot/config.ts +61 -0
  629. package/src/drivers/copilot/index.ts +310 -0
  630. package/src/drivers/cursor/README.md +66 -0
  631. package/src/drivers/cursor/config.ts +31 -0
  632. package/src/drivers/cursor/index.ts +326 -0
  633. package/src/drivers/cursor/interaction.ts +74 -0
  634. package/src/drivers/grok-build/README.md +112 -0
  635. package/src/drivers/grok-build/config.ts +43 -0
  636. package/src/drivers/grok-build/index.ts +331 -0
  637. package/src/drivers/mcp/index.ts +25 -11
  638. package/src/drivers/shared/acp-events.ts +94 -0
  639. package/src/drivers/shared/acp-interaction.ts +27 -0
  640. package/src/drivers/shared/acp-session.ts +40 -0
  641. package/src/drivers/shared/acp-transport.ts +187 -0
  642. package/src/drivers/shared/artifacts.ts +6 -2
  643. package/src/drivers/shared/experiment-behavior.ts +214 -0
  644. package/src/drivers/shared/experiment-policy.ts +195 -0
  645. package/src/drivers/subprocess/index.ts +55 -12
  646. package/src/index.ts +115 -10
  647. package/src/optimization/reflective.ts +3 -2
  648. package/src/orchestrator/annotation-loader.ts +10 -0
  649. package/src/orchestrator/annotation-store.ts +24 -3
  650. package/src/orchestrator/artifact-origin.ts +50 -0
  651. package/src/orchestrator/campaign-lifecycle.ts +5 -1
  652. package/src/orchestrator/candidates.ts +3 -2
  653. package/src/orchestrator/compare.ts +34 -1
  654. package/src/orchestrator/comparison-identity.ts +2 -0
  655. package/src/orchestrator/config.ts +20 -3
  656. package/src/orchestrator/discover.ts +8 -1
  657. package/src/orchestrator/doctor.ts +189 -28
  658. package/src/orchestrator/driver-capabilities.ts +21 -0
  659. package/src/orchestrator/driver-identity.ts +69 -0
  660. package/src/orchestrator/experiment-context.ts +3 -2
  661. package/src/orchestrator/experiment-corpus.ts +53 -37
  662. package/src/orchestrator/experiment-effects.ts +61 -0
  663. package/src/orchestrator/experiment-evaluation.ts +71 -10
  664. package/src/orchestrator/experiment-identity.ts +42 -48
  665. package/src/orchestrator/experiment-preflight.ts +27 -0
  666. package/src/orchestrator/experiment-report.ts +21 -1
  667. package/src/orchestrator/experiment-runs.ts +11 -4
  668. package/src/orchestrator/experiment-runtime-identity.ts +5 -243
  669. package/src/orchestrator/experiment-selection.ts +77 -0
  670. package/src/orchestrator/experiment.ts +29 -29
  671. package/src/orchestrator/fixture-identity.ts +9 -1
  672. package/src/orchestrator/fs-read.ts +2 -32
  673. package/src/orchestrator/grader-experiment.ts +74 -13
  674. package/src/orchestrator/grading.ts +35 -7
  675. package/src/orchestrator/harness-catalog-drift.ts +132 -0
  676. package/src/orchestrator/harness-registry.ts +12 -0
  677. package/src/orchestrator/index.ts +1 -0
  678. package/src/orchestrator/list-runs.ts +95 -11
  679. package/src/orchestrator/optimization-lifecycle.ts +4 -1
  680. package/src/orchestrator/orchestrator.ts +92 -216
  681. package/src/orchestrator/paired-effects.ts +209 -0
  682. package/src/orchestrator/provenance.ts +23 -3
  683. package/src/orchestrator/rubric-loader.ts +1 -1
  684. package/src/orchestrator/run-dir.ts +8 -1
  685. package/src/orchestrator/run-execution.ts +77 -11
  686. package/src/orchestrator/run-lifecycle.ts +4 -2
  687. package/src/orchestrator/run-workspace.ts +62 -0
  688. package/src/orchestrator/suite-compare.ts +29 -19
  689. package/src/orchestrator/suite-runs.ts +13 -2
  690. package/src/orchestrator/suite-summary.ts +122 -13
  691. package/src/previewer/artifacts.ts +221 -0
  692. package/src/previewer/server.ts +787 -0
  693. package/viewer/app.css +2075 -0
  694. package/viewer/app.js +627 -0
  695. package/viewer/artifact-view.js +48 -0
  696. package/viewer/charts.js +86 -0
  697. package/viewer/detail.js +1231 -0
  698. package/viewer/format.js +129 -0
  699. package/viewer/index.html +151 -0
  700. package/lib/answerers/scripted/index.d.ts +0 -8
  701. package/lib/answerers/scripted/index.d.ts.map +0 -1
  702. package/lib/answerers/scripted/index.js +0 -163
  703. package/lib/answerers/scripted/index.js.map +0 -1
  704. package/lib/cli/skills.d.ts +0 -38
  705. package/lib/cli/skills.d.ts.map +0 -1
  706. package/lib/cli/skills.js +0 -84
  707. package/lib/cli/skills.js.map +0 -1
  708. package/lib/drivers/shared/secure-copy.d.ts.map +0 -1
  709. package/lib/drivers/shared/secure-copy.js.map +0 -1
  710. package/skills/lux/SKILL.md +0 -85
  711. package/skills/lux/references/cli.md +0 -102
  712. package/skills/lux/references/eval-authoring.md +0 -105
  713. package/src/answerers/scripted/index.ts +0 -186
  714. package/src/cli/skills.ts +0 -129
  715. /package/lib/{drivers/shared → core}/secure-copy.d.ts +0 -0
  716. /package/lib/{drivers/shared → core}/secure-copy.js +0 -0
  717. /package/src/{drivers/shared → core}/secure-copy.ts +0 -0
@@ -2,47 +2,41 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { lstat, mkdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { createRequire } from "node:module";
5
- import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
6
- import { Command, InvalidArgumentError } from "commander";
7
- import { createRubricAssertion } from "../assertions/rubric/index.js";
5
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Cli, Errors, z } from "incur";
8
8
  import {
9
9
  type Candidate,
10
10
  GraderAnnotationSchema,
11
+ type HarnessFocus,
11
12
  matchesAnyGlob,
13
+ ReasoningEffortSchema,
12
14
  valueHash,
13
15
  } from "../core/index.js";
16
+ import { registryWithHarness } from "../orchestrator/harness-registry.js";
14
17
  import {
15
18
  applyCandidate,
16
19
  buildRegistryFromConfig,
17
20
  compareRuns,
18
21
  compareSuiteRuns,
19
- type DiscoveredCase,
22
+ DoctorReportSchema,
20
23
  discoverCases,
21
- doctorProject,
24
+ ExperimentCampaignPlanSchema,
25
+ ExperimentDecisionReportSchema,
26
+ ExperimentRunSchema,
22
27
  experimentCampaignMode,
23
28
  formatComparisonText,
24
- formatDoctorReport,
25
29
  formatExperimentDecisionMarkdown,
26
- formatExperimentRun,
27
- formatExperimentRunsTable,
28
- formatRunDetail,
29
- formatRunsTable,
30
30
  formatSuiteComparisonText,
31
- formatSuiteRunDetail,
32
- formatSuiteRunsTable,
31
+ GradingSnapshotSchema,
33
32
  hasRegression,
34
33
  hasSuiteRegression,
35
- isExperimentRun,
36
- listExperimentRuns,
37
- listRuns,
38
- listSuiteRuns,
39
34
  loadEvalCase,
40
35
  loadExperimentCampaign,
41
36
  loadExperimentRun,
42
37
  loadFrozenRegradableRun,
43
38
  loadLuxConfig,
44
39
  loadRubricDefinition,
45
- loadRunDetail,
46
40
  loadSuiteRun,
47
41
  loadVerifiedExperimentDecisionSnapshot,
48
42
  Orchestrator,
@@ -53,8 +47,6 @@ import {
53
47
  resolveExperimentCampaignPath,
54
48
  runExperimentCampaign,
55
49
  type SuiteCaseResult,
56
- type SuiteItem,
57
- selectCases,
58
50
  upsertGraderAnnotation,
59
51
  verifyExperimentDecisionIntegrity,
60
52
  verifyExperimentRunIntegrity,
@@ -65,573 +57,597 @@ import {
65
57
  failWith,
66
58
  relativePathWithin,
67
59
  } from "./command-runtime.js";
68
- import { formatInitReport, initProject } from "./init.js";
60
+ import { doctorCommand } from "./doctor.js";
61
+ import { initProject } from "./init.js";
62
+ import { CompareOutputSchema, ViewOutputSchema } from "./output-schemas.js";
69
63
  import { ConsoleReporter } from "./render/reporter.js";
70
64
  import {
71
- collectTags,
72
- collectValues,
65
+ expandLoop,
73
66
  parseAnnotationAssertion,
74
- parsePositiveInt,
75
- parseUnitInterval,
67
+ partitionFilters,
68
+ planSuiteItems,
76
69
  type RunOptions,
77
70
  resolveAnswererOverride,
78
71
  resolveConcurrency,
72
+ selectSuite,
73
+ suiteTitle,
79
74
  } from "./run-options.js";
80
- import {
81
- type BundledSkillName,
82
- formatSkillsInstall,
83
- formatSkillsList,
84
- installSkills,
85
- type SkillsPlatform,
86
- } from "./skills.js";
75
+ import { type ViewOptions, viewCommand, webViewCommand } from "./view.js";
87
76
 
88
77
  const requireFromHere = createRequire(import.meta.url);
89
78
  // From lib/cli/program.js (or src/cli/program.ts under test) to the package root.
90
79
  const PKG_VERSION = (requireFromHere("../../package.json") as { version: string }).version;
91
80
 
92
- const registerProjectCommands = (program: Command): void => {
93
- program
94
- .command("init")
95
- .description("scaffold a new evals project in the current directory")
96
- .option("--force", "overwrite existing files")
97
- .option("--experiment", "also scaffold a prompt optimization campaign and split cases")
98
- .action(async (options: { force?: boolean; experiment?: boolean }) => {
99
- const cwd = process.cwd();
100
- const result = await initProject({
101
- cwd,
102
- force: options.force ?? false,
103
- experiment: options.experiment ?? false,
104
- });
105
- process.stdout.write(`${formatInitReport(result, cwd)}\n`);
106
- });
107
-
108
- const skills = program.command("skills").description("manage bundled agent skills");
109
-
110
- skills
111
- .command("list")
112
- .description("list bundled skills and supported platforms")
113
- .action(() => {
114
- process.stdout.write(`${formatSkillsList()}\n`);
115
- });
116
-
117
- skills
118
- .command("install")
119
- .description("install the Lux eval-authoring skill for Claude Code or Codex")
120
- .argument("<platform>", "claude, codex, or all")
121
- .option("--project", "install into the current project instead of the user profile")
122
- .option("--dir <path>", "install under an explicit skills directory")
123
- .option("--name <name>", "install one bundled skill")
124
- .action(
125
- async (
126
- platform: string,
127
- options: {
128
- project?: boolean;
129
- dir?: string;
130
- name?: string;
81
+ const nonEmptyString = z.string().min(1);
82
+ const positiveInteger = z.coerce.number().int().positive();
83
+ const unitInterval = z.coerce.number().min(0).max(1);
84
+ const tagList = z
85
+ .array(nonEmptyString)
86
+ .default([])
87
+ .describe("Repeatable list; each value may be comma-separated");
88
+ const normalizeTags = (values: string[]): string[] =>
89
+ values.flatMap((value) => value.split(",").map((tag) => tag.trim())).filter(Boolean);
90
+ const initOutput = z.object({
91
+ created: z.array(z.string()),
92
+ updated: z.array(z.string()),
93
+ skipped: z.array(z.object({ path: z.string(), reason: z.string() })),
94
+ experiment: z.boolean(),
95
+ });
96
+ const runOutput = z.union([
97
+ z.object({
98
+ caseCount: z.number().int(),
99
+ cases: z.array(
100
+ z.object({
101
+ path: z.string(),
102
+ id: z.string(),
103
+ caseId: z.string().optional(),
104
+ permutationId: z.string().optional(),
105
+ }),
106
+ ),
107
+ }),
108
+ z.object({
109
+ caseCount: z.number().int(),
110
+ tags: z.array(z.object({ tag: z.string(), count: z.number().int() })),
111
+ }),
112
+ z.object({
113
+ suiteDir: z.string(),
114
+ results: z.array(
115
+ z.object({
116
+ caseId: z.string(),
117
+ runDir: z.string().nullable(),
118
+ exitReason: z.string(),
119
+ casePassed: z.boolean().nullable(),
120
+ caseScore: z.number().nullable(),
121
+ assertionsPassed: z.number().int(),
122
+ assertionsTotal: z.number().int(),
123
+ }),
124
+ ),
125
+ }),
126
+ ]);
127
+ const experimentOutput = z.union([
128
+ ExperimentCampaignPlanSchema,
129
+ ExperimentRunSchema.extend({ experimentDir: z.string() }),
130
+ ]);
131
+ const gradingOutput = GradingSnapshotSchema.extend({ gradingPath: z.string() });
132
+ const reportOutput = z.union([
133
+ ExperimentDecisionReportSchema,
134
+ z.object({ path: z.string(), report: ExperimentDecisionReportSchema }),
135
+ ]);
136
+
137
+ const campaignArgs = z.object({ campaign: nonEmptyString.describe("Experiment campaign path") });
138
+ const campaignOptions = z.object({
139
+ runsRoot: nonEmptyString.optional().describe("Run directory root overriding lux.config.ts"),
140
+ resume: nonEmptyString.optional().describe("Interrupted experiment directory to resume"),
141
+ plan: z.boolean().optional().describe("Validate and estimate without calling models"),
142
+ });
143
+
144
+ export const buildCli = () =>
145
+ Cli.create("lux", {
146
+ description:
147
+ "Coding-agent evaluation and improvement with deterministic assertions and human-in-the-loop runs",
148
+ version: PKG_VERSION,
149
+ sync: {
150
+ include: [fileURLToPath(new URL("../../skills/*", import.meta.url))],
151
+ depth: 2,
152
+ suggestions: [
153
+ "Use Lux to inspect the eval cases in this project",
154
+ "Use Lux to diagnose the latest failed eval run",
155
+ "Use Lux to plan an evidence-based improvement experiment",
156
+ ],
157
+ },
158
+ mcp: {
159
+ title: "Lux coding-agent evaluation",
160
+ instructions:
161
+ "Inspect and plan before running model-spending commands. Apply only a verified promoted experiment champion.",
162
+ },
163
+ })
164
+ .command("init", {
165
+ description: "Scaffold a new eval project in the current directory",
166
+ options: z.object({
167
+ force: z.boolean().optional().describe("Overwrite existing files"),
168
+ experiment: z.boolean().optional().describe("Also scaffold an optimization campaign"),
169
+ driver: z
170
+ .enum([
171
+ "subprocess",
172
+ "claude-code",
173
+ "codex",
174
+ "cursor",
175
+ "copilot",
176
+ "antigravity",
177
+ "grok-build",
178
+ ])
179
+ .optional()
180
+ .describe("Starter driver (default: deterministic subprocess)"),
181
+ model: nonEmptyString.optional().describe("Explicit subject model; required for Cursor"),
182
+ }),
183
+ output: initOutput,
184
+ destructive: true,
185
+ mcp: { annotations: { destructiveHint: true, openWorldHint: false } },
186
+ examples: [
187
+ { description: "Scaffold a basic eval project" },
188
+ { options: { experiment: true }, description: "Scaffold an optimization project" },
189
+ ],
190
+ run: async (c) =>
191
+ c.ok(await initCommand(c.options), { cta: { commands: ["doctor", "run --list"] } }),
192
+ })
193
+ .command("doctor", {
194
+ description: "Preflight project configuration, CLIs, cases, campaigns, and budgets",
195
+ args: z.object({ campaigns: z.array(z.string()).default([]).describe("Campaign paths") }),
196
+ options: z.object({
197
+ static: z
198
+ .boolean()
199
+ .optional()
200
+ .describe("Validate project files without checking installed executables (for CI)"),
201
+ }),
202
+ output: DoctorReportSchema,
203
+ mcp: {
204
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
205
+ },
206
+ run: async (c) =>
207
+ c.ok(await doctorCommand(c.args.campaigns, c.options), {
208
+ cta: { commands: ["run --list"] },
209
+ }),
210
+ })
211
+ .command("run", {
212
+ description: "Discover and run cases, filtered by name, path, or tag",
213
+ args: z.object({ filters: z.array(z.string()).default([]).describe("Case names or paths") }),
214
+ options: z.object({
215
+ tag: tagList.describe("Required case tag; repeatable and comma-separated"),
216
+ loop: positiveInteger.optional().describe("Run each matched eval this many times"),
217
+ concurrency: positiveInteger.optional().describe("Maximum cases running concurrently"),
218
+ list: z.boolean().optional().describe("List matching cases without running"),
219
+ listTags: z.boolean().optional().describe("List tags across matching cases"),
220
+ interactive: z.boolean().optional().describe("Answer agent questions in this terminal"),
221
+ claudeAnswerer: z.boolean().optional().describe("Use Claude Code as the answerer"),
222
+ skipJudge: z.boolean().optional().describe("Skip rubric LLM judge assertions"),
223
+ runsRoot: nonEmptyString.optional().describe("Run directory root"),
224
+ profile: nonEmptyString.optional().describe("Named driver and answerer profile"),
225
+ driver: nonEmptyString
226
+ .optional()
227
+ .describe("Focus on one harness driver, replacing the matrix"),
228
+ model: nonEmptyString.optional().describe("Focus on one subject model"),
229
+ effort: ReasoningEffortSchema.optional().describe("Focus on one reasoning effort"),
230
+ subjectRoot: nonEmptyString.optional().describe("Root bound into subjectPath values"),
231
+ verbose: z.boolean().optional().describe("Include phase timings and run directories"),
232
+ }),
233
+ alias: { tag: "t", concurrency: "j", interactive: "i", verbose: "v" },
234
+ output: runOutput,
235
+ outputPolicy: "agent-only",
236
+ mcp: { annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true } },
237
+ examples: [
238
+ { options: { list: true, tag: [] }, description: "List all discovered cases" },
239
+ {
240
+ args: { filters: ["smoke"] },
241
+ options: { tag: [] },
242
+ description: "Run matching cases",
131
243
  },
132
- ) => {
133
- if (!["claude", "claude-code", "codex", "all"].includes(platform)) {
134
- throw new InvalidArgumentError("platform must be claude, codex, or all");
135
- }
136
- if (options.name && !["lux", "lux-answerer"].includes(options.name)) {
137
- throw new InvalidArgumentError("skill name must be lux or lux-answerer");
138
- }
139
- const cwd = process.cwd();
140
- const installed = await installSkills({
141
- cwd,
142
- platform: platform as SkillsPlatform,
143
- project: options.project ?? false,
144
- ...(options.dir ? { dir: options.dir } : {}),
145
- ...(options.name ? { name: options.name as BundledSkillName } : {}),
244
+ {
245
+ args: { filters: ["smoke"] },
246
+ options: { tag: [], driver: "codex", model: "gpt-5.6-luna", effort: "low" },
247
+ description: "Focus on one harness permutation",
248
+ },
249
+ ],
250
+ run: async (c) => {
251
+ const result = await runCommand(c.args.filters, c.options as RunOptions, !c.agent);
252
+ const suiteDir = "suiteDir" in result ? result.suiteDir : undefined;
253
+ return c.ok(result, {
254
+ cta: {
255
+ description: "Inspect saved results:",
256
+ commands: suiteDir ? [{ command: "view", args: { runDir: suiteDir } }] : [],
257
+ },
146
258
  });
147
- process.stdout.write(`${formatSkillsInstall(installed, cwd)}\n`);
148
259
  },
149
- );
150
- };
151
-
152
- const registerExperimentCommands = (program: Command): void => {
153
- program
154
- .command("doctor [campaigns...]")
155
- .description("preflight project configuration, CLIs, cases, campaigns, and budgets")
156
- .option("--json", "emit machine-readable JSON")
157
- .action(async (campaigns: string[], options: { json?: boolean }) => {
158
- const report = await doctorProject({ cwd: process.cwd(), campaigns });
159
- process.stdout.write(
160
- options.json ? `${JSON.stringify(report, null, 2)}\n` : `${formatDoctorReport(report)}\n`,
161
- );
162
- if (!report.ok) failWith(1);
260
+ })
261
+ .command("experiment", {
262
+ description: "Evaluate controlled source variants across cases and model profiles",
263
+ args: campaignArgs,
264
+ options: campaignOptions,
265
+ output: experimentOutput,
266
+ mcp: { annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true } },
267
+ run: async (c) => experimentCommand("experiment", c.args.campaign, c.options),
268
+ })
269
+ .command("optimize", {
270
+ description: "Evolve candidates from grading feedback and promote on held-out cases",
271
+ args: campaignArgs,
272
+ options: campaignOptions,
273
+ output: experimentOutput,
274
+ destructive: true,
275
+ mcp: { annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true } },
276
+ run: async (c) => experimentCommand("optimize", c.args.campaign, c.options),
277
+ })
278
+ .command("grade", {
279
+ description: "Re-run current graders against a persisted run",
280
+ args: z.object({ runDir: nonEmptyString.describe("Persisted run directory") }),
281
+ options: z.object({ case: nonEmptyString.optional().describe("Current case definition") }),
282
+ output: gradingOutput,
283
+ mcp: { annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true } },
284
+ run: async (c) => gradeCommand(c.args.runDir, c.options),
285
+ })
286
+ .command("annotate", {
287
+ description: "Add or update a human grader-alignment label",
288
+ args: z.object({ runDir: nonEmptyString.describe("Persisted run directory") }),
289
+ options: z.object({
290
+ out: nonEmptyString.describe("JSON annotation dataset to create or update"),
291
+ rubric: nonEmptyString.describe("Candidate-relative declarative rubric path"),
292
+ id: nonEmptyString.optional().describe("Stable annotation ID"),
293
+ label: z.enum(["pass", "fail"]).optional().describe("Expected overall verdict"),
294
+ score: unitInterval.optional().describe("Expected score from zero to one"),
295
+ assertion: z
296
+ .array(nonEmptyString)
297
+ .default([])
298
+ .describe("Assertion label id=pass|fail[:score]"),
299
+ split: z.enum(["train", "validation", "test"]).optional().describe("Dataset split"),
300
+ tag: tagList.describe("Selection tag; repeatable and comma-separated"),
301
+ feedback: nonEmptyString.optional().describe("Human rationale for mismatches"),
302
+ annotator: nonEmptyString.optional().describe("Label author"),
303
+ reviewer: nonEmptyString.optional().describe("Independent reviewer"),
304
+ source: z.enum(["human", "synthetic"]).default("human").describe("Label provenance"),
305
+ }),
306
+ output: z.object({ id: z.string(), path: z.string(), labels: z.number().int() }),
307
+ destructive: true,
308
+ mcp: {
309
+ annotations: {
310
+ readOnlyHint: false,
311
+ destructiveHint: false,
312
+ idempotentHint: true,
313
+ openWorldHint: false,
314
+ },
315
+ },
316
+ run: async (c) => annotateCommand(c.args.runDir, c.options),
317
+ })
318
+ .command("apply", {
319
+ description: "Apply the verified promoted experiment champion",
320
+ args: z.object({
321
+ experimentDir: nonEmptyString.describe("Experiment directory"),
322
+ candidateId: nonEmptyString.optional().describe("Expected promoted candidate ID"),
323
+ }),
324
+ output: z.object({
325
+ candidateId: z.string(),
326
+ subjectRoot: z.string(),
327
+ sourceBytes: z.number(),
328
+ }),
329
+ destructive: true,
330
+ mcp: {
331
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
332
+ },
333
+ run: async (c) => applyCommand(c.args.experimentDir, c.args.candidateId),
334
+ })
335
+ .command("report", {
336
+ description: "Render a PR-ready report from a verified experiment record",
337
+ args: z.object({ experimentDir: nonEmptyString.describe("Experiment directory") }),
338
+ options: z.object({
339
+ out: nonEmptyString.optional().describe("Write Markdown to this path"),
340
+ force: z.boolean().optional().describe("Overwrite an existing output file"),
341
+ }),
342
+ output: reportOutput,
343
+ mcp: { annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } },
344
+ run: async (c) => reportCommand(c.args.experimentDir, c.options),
345
+ })
346
+ .command("view", {
347
+ description: "List past runs or show one run in detail",
348
+ args: z.object({ runDir: nonEmptyString.optional().describe("Run or suite directory") }),
349
+ options: z.object({
350
+ runsRoot: nonEmptyString.optional().describe("Run directory root"),
351
+ case: nonEmptyString.optional().describe("Filter lists to one case slug"),
352
+ suites: z.boolean().optional().describe("List logical suite runs"),
353
+ experiments: z.boolean().optional().describe("List experiment runs"),
354
+ web: z.boolean().optional().describe("Open the Lux Review web app"),
355
+ port: z.coerce.number().int().min(0).max(65535).optional().describe("Review server port"),
356
+ open: z.boolean().default(true).describe("Open a browser in web mode"),
357
+ }),
358
+ output: ViewOutputSchema,
359
+ mcp: { annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } },
360
+ run: (c) => {
361
+ if (c.options.web) {
362
+ if (c.formatExplicit && c.format !== "jsonl") {
363
+ throw new Errors.ParseError({
364
+ message: "--web requires streaming output; omit --format or use --format jsonl",
365
+ });
366
+ }
367
+ return webViewCommand(c.args.runDir, c.options as ViewOptions);
368
+ }
369
+ return viewCommand(c.args.runDir, c.options as ViewOptions).then((result) =>
370
+ ViewOutputSchema.parse(result),
371
+ );
372
+ },
373
+ })
374
+ .command("compare", {
375
+ description: "Diff two run or suite directories and fail on regression",
376
+ args: z.object({
377
+ runA: nonEmptyString.describe("Baseline run"),
378
+ runB: nonEmptyString.describe("Current run"),
379
+ }),
380
+ options: z.object({
381
+ passRateTolerance: unitInterval.optional().describe("Allowed per-case pass-rate decrease"),
382
+ }),
383
+ output: CompareOutputSchema,
384
+ mcp: { annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } },
385
+ run: async (c) =>
386
+ CompareOutputSchema.parse(await compareCommand(c.args.runA, c.args.runB, c.options)),
163
387
  });
164
388
 
165
- program
166
- .command("run [filters...]", { isDefault: true })
167
- .description("discover and run cases; filter by name/path substring or --tag")
168
- .option(
169
- "-t, --tag <tag>",
170
- "only cases whose meta.tags include this tag (repeatable, comma-separated)",
171
- collectTags,
172
- [],
173
- )
174
- .option("--loop <n>", "run each matched eval n times", parsePositiveInt)
175
- .option(
176
- "-j, --concurrency <n>",
177
- "run up to n cases at once (overrides lux.config.ts)",
178
- parsePositiveInt,
179
- )
180
- .option("--list", "print the matched cases and exit without running them")
181
- .option("--list-tags", "print the tags across the matched cases and exit")
182
- .option("-i, --interactive", "answer agent questions yourself in this terminal (HITL)")
183
- .option(
184
- "--claude-answerer",
185
- "delegate questions through the Claude plugin's lux-answerer skill",
186
- )
187
- .option("--runs-root <path>", "run-dir root (overrides lux.config.ts)")
188
- .option("--profile <id>", "named driver/answerer profile from lux.config.ts")
189
- .option("--subject-root <path>", "root bound into subjectPath() values")
190
- .option("-v, --verbose", "add phase timings and the run dir to every case")
191
- .action(async (filters: string[], options: RunOptions) => {
192
- await runCommand(filters, options);
389
+ const initCommand = async (options: Omit<Parameters<typeof initProject>[0], "cwd">) =>
390
+ initProject({ cwd: process.cwd(), ...options });
391
+
392
+ const gradeCommand = async (runDir: string, options: { case?: string | undefined }) => {
393
+ const directory = resolve(runDir);
394
+ if (!(await ensureDirectory("grade", directory))) failWith(1);
395
+ const config = await loadLuxConfig(process.cwd());
396
+ using controller = abortOnInterrupt();
397
+ const result = await regradeRun({
398
+ runDir: directory,
399
+ registry: buildRegistryFromConfig(config),
400
+ graderContext: config.harness ?? null,
401
+ ...(options.case ? { case: await loadEvalCase(resolve(options.case)) } : {}),
402
+ abortSignal: controller.signal,
403
+ });
404
+ if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
405
+ if (result.snapshot.report.casePassed !== true) {
406
+ throw new Errors.IncurError({
407
+ code: "GRADING_FAILED",
408
+ message: `The persisted run did not pass regrading. Grading saved to ${result.path}.`,
409
+ exitCode: 1,
193
410
  });
411
+ }
412
+ return { gradingPath: result.path, ...result.snapshot };
413
+ };
194
414
 
195
- program
196
- .command("experiment <campaign>")
197
- .description("evaluate controlled source variants across cases and model profiles")
198
- .option("--runs-root <path>", "run-dir root (overrides lux.config.ts)")
199
- .option("--resume <experiment-dir>", "resume an interrupted experiment ledger")
200
- .option("--plan", "validate and estimate the campaign without calling models")
201
- .option("--json", "emit machine-readable JSON")
202
- .action(
203
- async (
204
- campaign: string,
205
- options: { runsRoot?: string; resume?: string; json?: boolean; plan?: boolean },
206
- ) => {
207
- await experimentCommand("experiment", campaign, options);
208
- },
209
- );
415
+ type AnnotationOptions = {
416
+ out: string;
417
+ rubric: string;
418
+ id?: string | undefined;
419
+ label?: "pass" | "fail" | undefined;
420
+ score?: number | undefined;
421
+ assertion: string[];
422
+ split?: "train" | "validation" | "test" | undefined;
423
+ tag: string[];
424
+ feedback?: string | undefined;
425
+ annotator?: string | undefined;
426
+ reviewer?: string | undefined;
427
+ source: "human" | "synthetic";
210
428
  };
211
429
 
212
- const registerCandidateCommands = (program: Command): void => {
213
- program
214
- .command("grade <runDir>")
215
- .description("re-run current graders against a persisted run without rerunning the agent")
216
- .option("--case <path>", "grade with a current case definition instead of frozen case.json")
217
- .option("--json", "emit machine-readable JSON")
218
- .action(async (runDir: string, options: { case?: string; json?: boolean }) => {
219
- const directory = resolve(runDir);
220
- if (!(await ensureDirectory("grade", directory))) return;
221
- const config = await loadLuxConfig(process.cwd());
222
- const controller = abortOnInterrupt();
223
- const result = await regradeRun({
224
- runDir: directory,
225
- registry: buildRegistryFromConfig(config),
226
- graderContext: config.harness ?? null,
227
- ...(options.case ? { case: await loadEvalCase(resolve(options.case)) } : {}),
228
- abortSignal: controller.signal,
229
- });
230
- const report = result.snapshot.report;
231
- let gradingStatus = "ungraded";
232
- if (report.casePassed === true) gradingStatus = "PASS";
233
- if (report.casePassed === false) gradingStatus = "FAIL";
234
- let output: string;
235
- if (options.json) {
236
- output = `${JSON.stringify({ gradingPath: result.path, ...result.snapshot }, null, 2)}\n`;
237
- } else {
238
- const score = report.caseScore === null ? "—" : `${(report.caseScore * 100).toFixed(0)}%`;
239
- output = [
240
- `grading: ${gradingStatus}`,
241
- `score: ${score}`,
242
- `checks: ${report.assertions.filter((assertion) => assertion.passed).length}/${report.assertions.length}`,
243
- `saved: ${result.path}`,
244
- "",
245
- ].join("\n");
246
- }
247
- process.stdout.write(output);
248
- if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
249
- else if (report.casePassed !== true) failWith(1);
430
+ const annotateCommand = async (runDir: string, options: AnnotationOptions) => {
431
+ const directory = resolve(runDir);
432
+ if (!(await ensureDirectory("annotate", directory))) failWith(1);
433
+ const parsedAssertions = options.assertion.map(parseAnnotationAssertion);
434
+ const assertionIds = parsedAssertions.map(([id]) => id);
435
+ if (new Set(assertionIds).size !== assertionIds.length) {
436
+ throw new Errors.ParseError({ message: "--assertion IDs must not be repeated" });
437
+ }
438
+ const assertions = Object.fromEntries(parsedAssertions);
439
+ if (
440
+ options.label === undefined &&
441
+ options.score === undefined &&
442
+ Object.keys(assertions).length === 0
443
+ ) {
444
+ throw new Errors.ParseError({
445
+ message: "Provide --label, --score, or at least one --assertion",
250
446
  });
251
-
252
- program
253
- .command("annotate <runDir>")
254
- .description("add or update a human grader-alignment label for a persisted run")
255
- .requiredOption("--out <path>", "JSON annotation dataset to create or update")
256
- .requiredOption("--rubric <path>", "candidate-relative declarative .json rubric path")
257
- .option("--id <id>", "stable annotation id")
258
- .option("--label <pass|fail>", "expected overall case verdict")
259
- .option("--score <n>", "expected overall case score from 0 to 1", parseUnitInterval)
260
- .option(
261
- "--assertion <id=pass|fail[:score]>",
262
- "expected stable assertion verdict and optional score (repeatable)",
263
- collectValues,
264
- [],
265
- )
266
- .option("--split <train|validation|test>", "dataset split, stored as a tag")
267
- .option("--tag <tag>", "extra selection tag (repeatable, comma-separated)", collectTags, [])
268
- .option("--feedback <text>", "human rationale shown to the rubric optimizer on mismatch")
269
- .option("--annotator <id>", "person or system that authored the label")
270
- .option("--reviewer <id>", "independent reviewer of the label")
271
- .option("--source <human|synthetic>", "label provenance", "human")
272
- .action(
273
- async (
274
- runDir: string,
275
- options: {
276
- out: string;
277
- rubric: string;
278
- id?: string;
279
- label?: string;
280
- score?: number;
281
- assertion: string[];
282
- split?: string;
283
- tag: string[];
284
- feedback?: string;
285
- annotator?: string;
286
- reviewer?: string;
287
- source: string;
288
- },
289
- ) => {
290
- const directory = resolve(runDir);
291
- if (!(await ensureDirectory("annotate", directory))) return;
292
- if (options.label && options.label !== "pass" && options.label !== "fail") {
293
- throw new InvalidArgumentError("--label must be pass or fail");
294
- }
295
- if (options.split && !["train", "validation", "test"].includes(options.split)) {
296
- throw new InvalidArgumentError("--split must be train, validation, or test");
297
- }
298
- if (options.source !== "human" && options.source !== "synthetic") {
299
- throw new InvalidArgumentError("--source must be human or synthetic");
300
- }
301
- const parsedAssertions = options.assertion.map(parseAnnotationAssertion);
302
- const assertionIds = parsedAssertions.map(([id]) => id);
303
- if (new Set(assertionIds).size !== assertionIds.length) {
304
- throw new InvalidArgumentError("--assertion ids must not be repeated");
305
- }
306
- const assertions = Object.fromEntries(parsedAssertions);
307
- if (
308
- options.label === undefined &&
309
- options.score === undefined &&
310
- Object.keys(assertions).length === 0
311
- ) {
312
- throw new Error("lux annotate: provide --label, --score, or at least one --assertion");
313
- }
314
- const out = resolve(options.out);
315
- const run = await loadFrozenRegradableRun(directory);
316
- try {
317
- const rubric = await loadRubricDefinition(
318
- await resolveAuthoredFile(options.rubric, "rubric"),
319
- );
320
- if (rubric.id !== run.case.id) {
321
- throw new Error(
322
- `lux annotate: rubric id '${rubric.id}' does not match persisted case '${run.case.id}'`,
323
- );
324
- }
325
- const rubricIds = new Set(rubric.assertions.map((assertion) => assertion.id));
326
- const unknownIds = assertionIds.filter((assertionId) => !rubricIds.has(assertionId));
327
- if (unknownIds.length > 0) {
328
- throw new Error(`lux annotate: unknown rubric assertion ids: ${unknownIds.join(", ")}`);
329
- }
330
- const tags = [...(options.split ? [options.split] : []), ...options.tag].filter(
331
- (tag, index, all) => all.indexOf(tag) === index,
332
- );
333
- const caseId = run.case.id;
334
- const portableRunDir = (relative(dirname(out), directory) || ".").split(sep).join("/");
335
- const id =
336
- options.id ??
337
- `${caseId}-${basename(directory)}-${valueHash(portableRunDir).slice(0, 10)}`.replace(
338
- /[^a-zA-Z0-9_-]+/g,
339
- "-",
340
- );
341
- const annotation = GraderAnnotationSchema.parse({
342
- id,
343
- runDir: portableRunDir,
344
- rubricPath: options.rubric,
345
- tags,
346
- source: options.source,
347
- ...(options.annotator ? { annotator: options.annotator } : {}),
348
- labeledAt: new Date().toISOString(),
349
- ...(options.reviewer ? { reviewer: options.reviewer } : {}),
350
- expected: {
351
- ...(options.label ? { casePassed: options.label === "pass" } : {}),
352
- ...(options.score !== undefined ? { caseScore: options.score } : {}),
353
- assertions,
354
- },
355
- ...(options.feedback ? { feedback: options.feedback } : {}),
356
- });
357
- const dataset = await upsertGraderAnnotation(out, annotation);
358
- process.stdout.write(
359
- `annotated ${annotation.id} in ${out} (${dataset.annotations.length} labels)\n`,
360
- );
361
- } finally {
362
- await releaseFrozenRegradableRun(run);
363
- }
447
+ }
448
+ const out = resolve(options.out);
449
+ const run = await loadFrozenRegradableRun(directory);
450
+ try {
451
+ const rubric = await loadRubricDefinition(await resolveAuthoredFile(options.rubric, "rubric"));
452
+ if (rubric.id !== run.case.id) {
453
+ throw new Error(`Rubric ID '${rubric.id}' does not match persisted case '${run.case.id}'.`);
454
+ }
455
+ const rubricIds = new Set(rubric.assertions.map((assertion) => assertion.id));
456
+ const unknownIds = assertionIds.filter((assertionId) => !rubricIds.has(assertionId));
457
+ if (unknownIds.length > 0) {
458
+ throw new Errors.ParseError({
459
+ message: `Unknown rubric assertion IDs: ${unknownIds.join(", ")}`,
460
+ });
461
+ }
462
+ const annotationTags = [
463
+ ...(options.split ? [options.split] : []),
464
+ ...normalizeTags(options.tag),
465
+ ].filter((tag, index, all) => all.indexOf(tag) === index);
466
+ const portableRunDir = (relative(dirname(out), directory) || ".").split(sep).join("/");
467
+ const id =
468
+ options.id ??
469
+ `${run.case.id}-${basename(directory)}-${valueHash(portableRunDir).slice(0, 10)}`.replace(
470
+ /[^a-zA-Z0-9_-]+/g,
471
+ "-",
472
+ );
473
+ const annotation = GraderAnnotationSchema.parse({
474
+ id,
475
+ runDir: portableRunDir,
476
+ rubricPath: options.rubric,
477
+ tags: annotationTags,
478
+ source: options.source,
479
+ ...(options.annotator ? { annotator: options.annotator } : {}),
480
+ labeledAt: new Date().toISOString(),
481
+ ...(options.reviewer ? { reviewer: options.reviewer } : {}),
482
+ expected: {
483
+ ...(options.label ? { casePassed: options.label === "pass" } : {}),
484
+ ...(options.score !== undefined ? { caseScore: options.score } : {}),
485
+ assertions,
364
486
  },
365
- );
487
+ ...(options.feedback ? { feedback: options.feedback } : {}),
488
+ });
489
+ const dataset = await upsertGraderAnnotation(out, annotation);
490
+ return { id: annotation.id, path: out, labels: dataset.annotations.length };
491
+ } finally {
492
+ await releaseFrozenRegradableRun(run);
493
+ }
366
494
  };
367
495
 
368
- const registerReportingCommands = (program: Command): void => {
369
- program
370
- .command("optimize <campaign>")
371
- .description("evolve source candidates from grading feedback and promote on held-out cases")
372
- .option("--runs-root <path>", "run-dir root (overrides lux.config.ts)")
373
- .option("--resume <experiment-dir>", "resume an interrupted optimization ledger")
374
- .option("--plan", "validate and estimate the campaign without calling models")
375
- .option("--json", "emit machine-readable JSON")
376
- .action(
377
- async (
378
- campaign: string,
379
- options: { runsRoot?: string; resume?: string; json?: boolean; plan?: boolean },
380
- ) => {
381
- await experimentCommand("optimize", campaign, options);
382
- },
383
- );
384
-
385
- program
386
- .command("apply <experimentDir> [candidateId]")
387
- .description("apply the promoted experiment champion after verifying the source base hash")
388
- .action(async (experimentDir: string, candidateId?: string) => {
389
- const directory = resolve(experimentDir);
390
- const experiment = await loadExperimentRun(directory);
391
- if (!experiment) {
392
- process.stderr.write(`lux apply: experiment manifest not found at ${directory}\n`);
393
- failWith(1);
394
- return;
395
- }
396
- if (experiment.status !== "passed") {
397
- process.stderr.write(
398
- `lux apply: experiment status is '${experiment.status}', not 'passed'\n`,
399
- );
400
- failWith(1);
401
- return;
402
- }
403
- const championId = experiment.championCandidateId;
404
- if (!championId) {
405
- process.stderr.write("lux apply: passed experiment has no promoted champion\n");
406
- failWith(1);
407
- return;
408
- }
409
- if (candidateId && candidateId !== championId) {
410
- process.stderr.write(
411
- `lux apply: candidate '${candidateId}' is not the promoted champion '${championId}'\n`,
412
- );
413
- failWith(1);
414
- return;
415
- }
416
- let candidate: Candidate | undefined;
417
- try {
418
- const candidates = await verifyExperimentRunIntegrity(directory, experiment);
419
- verifyExperimentDecisionIntegrity(experiment);
420
- candidate = candidates.find((entry) => entry.id === championId);
421
- if (!candidate) {
422
- throw new Error(`champion candidate '${championId}' not found`);
423
- }
424
- } catch (error) {
425
- const message = error instanceof Error ? error.message : String(error);
426
- process.stderr.write(`lux apply: experiment integrity check failed: ${message}\n`);
427
- failWith(1);
428
- return;
429
- }
430
- await applyCandidate(experiment.subjectRoot, experiment.campaignSnapshot.subject, candidate);
431
- process.stdout.write(
432
- `applied candidate ${candidate.id} to ${experiment.subjectRoot} (${candidate.sourceBytes} bytes)\n`,
433
- );
496
+ const applyCommand = async (experimentDir: string, candidateId?: string) => {
497
+ const directory = resolve(experimentDir);
498
+ const experiment = await loadExperimentRun(directory);
499
+ if (!experiment) {
500
+ throw new Errors.IncurError({
501
+ code: "EXPERIMENT_NOT_FOUND",
502
+ message: `Experiment manifest not found at ${directory}.`,
503
+ exitCode: 1,
434
504
  });
435
-
436
- program
437
- .command("report <experimentDir>")
438
- .description("render a PR-ready promotion report from a verified experiment record")
439
- .option("--json", "emit the structured decision report as JSON")
440
- .option("--out <path>", "write the report to a file instead of stdout")
441
- .option("--force", "overwrite an existing output file")
442
- .action(
443
- async (experimentDir: string, options: { json?: boolean; out?: string; force?: boolean }) => {
444
- const directory = resolve(experimentDir);
445
- if (!(await ensureDirectory("report", directory))) return;
446
- const snapshot = await loadVerifiedExperimentDecisionSnapshot(directory);
447
- const { experiment, report } = snapshot;
448
- const rendered = options.json
449
- ? `${JSON.stringify(report, null, 2)}\n`
450
- : `${formatExperimentDecisionMarkdown(report)}\n`;
451
- if (!options.out) {
452
- process.stdout.write(rendered);
453
- return;
454
- }
455
- const out = resolve(options.out);
456
- const outputStats = await lstat(out).catch((error: NodeJS.ErrnoException) => {
457
- if (error.code === "ENOENT") return null;
458
- throw error;
459
- });
460
- if (outputStats && (outputStats.isSymbolicLink() || !outputStats.isFile())) {
461
- throw new Error("lux report: an existing --out must be a regular, non-symlink file");
462
- }
463
- const [physicalDirectory, physicalSubjectRoot, physicalOut] = await Promise.all([
464
- realpath(directory),
465
- realpath(experiment.subjectRoot),
466
- physicalDestinationPath(out),
467
- ]);
468
- assertReportDestination(
469
- physicalOut,
470
- physicalDirectory,
471
- physicalSubjectRoot,
472
- experiment.campaignSnapshot.subject.exclude,
473
- );
474
- if (outputStats && !options.force) {
475
- throw new Error(`lux report: output already exists (use --force): ${out}`);
476
- }
477
- await mkdir(dirname(out), { recursive: true });
478
- const physicalParent = await realpath(dirname(out));
479
- const finalPhysicalOut = resolve(physicalParent, basename(out));
480
- assertReportDestination(
481
- finalPhysicalOut,
482
- physicalDirectory,
483
- physicalSubjectRoot,
484
- experiment.campaignSnapshot.subject.exclude,
485
- );
486
- await writeReportSafely(finalPhysicalOut, rendered, options.force ?? false);
487
- process.stdout.write(`wrote experiment report to ${out}\n`);
488
- },
489
- );
490
-
491
- program
492
- .command("view [runDir]")
493
- .description("list past runs, or show one run's detail when given a runDir")
494
- .option("--runs-root <path>", "run-dir root (defaults to lux.config.ts)")
495
- .option("--case <id>", "filter the run list to one case slug (list mode only)")
496
- .option("--suites", "list logical suite runs instead of individual runs")
497
- .option("--experiments", "list experiment runs instead of individual runs")
498
- .option("--json", "emit machine-readable JSON")
499
- .action(
500
- async (
501
- runDirArg: string | undefined,
502
- options: {
503
- runsRoot?: string;
504
- case?: string;
505
- suites?: boolean;
506
- experiments?: boolean;
507
- json?: boolean;
508
- },
509
- ) => {
510
- if (runDirArg) {
511
- const path = resolve(runDirArg);
512
- if (!(await ensureDirectory("view", path))) return;
513
- if (isExperimentRun(path)) {
514
- const experiment = await loadExperimentRun(path);
515
- if (!experiment) {
516
- process.stderr.write(`lux view: invalid experiment at ${path}\n`);
517
- failWith(1);
518
- return;
519
- }
520
- process.stdout.write(
521
- options.json
522
- ? `${JSON.stringify({ experimentDir: path, ...experiment }, null, 2)}\n`
523
- : `${formatExperimentRun(path, experiment)}\n`,
524
- );
525
- return;
526
- }
527
- const suite = await loadSuiteRun(path);
528
- if (suite) {
529
- process.stdout.write(
530
- options.json
531
- ? `${JSON.stringify({ suiteDir: path, ...suite }, null, 2)}\n`
532
- : `${formatSuiteRunDetail(path, suite)}\n`,
533
- );
534
- return;
535
- }
536
- const detail = await loadRunDetail(path);
537
- if (options.json) {
538
- process.stdout.write(`${JSON.stringify(detail, null, 2)}\n`);
539
- } else {
540
- process.stdout.write(`${formatRunDetail(detail)}\n`);
541
- }
542
- return;
543
- }
544
-
545
- const cwd = process.cwd();
546
- const config = await loadLuxConfig(cwd);
547
- const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
548
- if (options.experiments) {
549
- const experiments = await listExperimentRuns(runsRoot);
550
- process.stdout.write(
551
- options.json
552
- ? `${JSON.stringify(experiments, null, 2)}\n`
553
- : `experiments at ${runsRoot}:\n${formatExperimentRunsTable(experiments)}\n`,
554
- );
555
- return;
556
- }
557
- if (options.suites) {
558
- const suites = await listSuiteRuns(runsRoot);
559
- process.stdout.write(
560
- options.json
561
- ? `${JSON.stringify(suites, null, 2)}\n`
562
- : `suite runs at ${runsRoot}:\n${formatSuiteRunsTable(suites)}\n`,
563
- );
564
- return;
565
- }
566
- const all = await listRuns(runsRoot);
567
- const runs = options.case ? all.filter((r) => r.caseSlug === options.case) : all;
568
- if (options.json) {
569
- process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
570
- } else {
571
- if (!options.case) {
572
- process.stdout.write(`suite runs at ${runsRoot}:\n`);
573
- process.stdout.write(`${formatSuiteRunsTable(await listSuiteRuns(runsRoot))}\n\n`);
574
- }
575
- process.stdout.write(`runs at ${runsRoot}:\n`);
576
- process.stdout.write(`${formatRunsTable(runs)}\n`);
577
- }
578
- },
579
- );
580
-
581
- program
582
- .command("compare <runA> <runB>")
583
- .description("diff two run or suite directories")
584
- .option(
585
- "--pass-rate-tolerance <fraction>",
586
- "allowed per-case pass-rate decrease for suite comparison",
587
- parseUnitInterval,
588
- )
589
- .action(async (runA: string, runB: string, options: { passRateTolerance?: number }) => {
590
- const pathA = resolve(runA);
591
- const pathB = resolve(runB);
592
- // Validate both up front so one typo'd path in CI cannot read as
593
- // "0 regressed"; report every bad path, not just the first.
594
- const okA = await ensureDirectory("compare", pathA);
595
- const okB = await ensureDirectory("compare", pathB);
596
- if (!okA || !okB) return;
597
- const [suiteA, suiteB] = await Promise.all([loadSuiteRun(pathA), loadSuiteRun(pathB)]);
598
- if (Boolean(suiteA) !== Boolean(suiteB)) {
599
- process.stderr.write("lux compare: both paths must be runs or both must be suites\n");
600
- failWith(1);
601
- return;
602
- }
603
- if (suiteA && suiteB) {
604
- const comparison = await compareSuiteRuns(pathA, pathB, {
605
- ...(options.passRateTolerance !== undefined
606
- ? { passRateTolerance: options.passRateTolerance }
607
- : {}),
608
- });
609
- process.stdout.write(`${formatSuiteComparisonText(comparison)}\n`);
610
- if (hasSuiteRegression(comparison)) failWith(1);
611
- return;
612
- }
613
- const cmp = await compareRuns({ pathA, pathB });
614
- process.stdout.write(`${formatComparisonText(cmp)}\n`);
615
- if (hasRegression(cmp)) failWith(1);
505
+ }
506
+ if (experiment.status !== "passed") {
507
+ throw new Errors.IncurError({
508
+ code: "EXPERIMENT_NOT_PROMOTED",
509
+ message: `Experiment status is '${experiment.status}', not 'passed'.`,
510
+ exitCode: 1,
511
+ });
512
+ }
513
+ const championId = experiment.championCandidateId;
514
+ if (!championId) {
515
+ throw new Errors.IncurError({
516
+ code: "CHAMPION_NOT_FOUND",
517
+ message: "The passed experiment has no promoted champion.",
518
+ exitCode: 1,
519
+ });
520
+ }
521
+ if (candidateId && candidateId !== championId) {
522
+ throw new Errors.IncurError({
523
+ code: "CANDIDATE_NOT_PROMOTED",
524
+ message: `Candidate '${candidateId}' is not the promoted champion '${championId}'.`,
525
+ exitCode: 1,
616
526
  });
527
+ }
528
+ let candidate: Candidate | undefined;
529
+ try {
530
+ const candidates = await verifyExperimentRunIntegrity(directory, experiment);
531
+ verifyExperimentDecisionIntegrity(experiment);
532
+ candidate = candidates.find((entry) => entry.id === championId);
533
+ if (!candidate) throw new Error(`Champion candidate '${championId}' not found.`);
534
+ } catch (error) {
535
+ throw new Errors.IncurError({
536
+ code: "EXPERIMENT_INTEGRITY_FAILED",
537
+ message: error instanceof Error ? error.message : String(error),
538
+ exitCode: 1,
539
+ });
540
+ }
541
+ await applyCandidate(experiment.subjectRoot, experiment.campaignSnapshot.subject, candidate);
542
+ return {
543
+ candidateId: candidate.id,
544
+ subjectRoot: experiment.subjectRoot,
545
+ sourceBytes: candidate.sourceBytes,
546
+ };
617
547
  };
618
548
 
619
- export const buildProgram = (): Command => {
620
- const program = new Command();
621
- program
622
- .name("lux")
623
- .description(
624
- "Coding-agent evaluation and improvement with deterministic assertions and human-in-the-loop runs",
625
- )
626
- .version(PKG_VERSION);
627
- registerProjectCommands(program);
628
- registerExperimentCommands(program);
629
- registerCandidateCommands(program);
630
- registerReportingCommands(program);
631
- return program;
549
+ const reportCommand = async (
550
+ experimentDir: string,
551
+ options: { out?: string | undefined; force?: boolean | undefined },
552
+ ) => {
553
+ const directory = resolve(experimentDir);
554
+ if (!(await ensureDirectory("report", directory))) failWith(1);
555
+ const snapshot = await loadVerifiedExperimentDecisionSnapshot(directory);
556
+ const { experiment, report } = snapshot;
557
+ if (!options.out) return report;
558
+ const out = resolve(options.out);
559
+ const outputStats = await lstat(out).catch((error: NodeJS.ErrnoException) => {
560
+ if (error.code === "ENOENT") return null;
561
+ throw error;
562
+ });
563
+ if (outputStats && (outputStats.isSymbolicLink() || !outputStats.isFile())) {
564
+ throw new Error("An existing --out must be a regular, non-symlink file.");
565
+ }
566
+ const [physicalDirectory, physicalSubjectRoot, physicalOut] = await Promise.all([
567
+ realpath(directory),
568
+ realpath(experiment.subjectRoot),
569
+ physicalDestinationPath(out),
570
+ ]);
571
+ assertReportDestination(
572
+ physicalOut,
573
+ physicalDirectory,
574
+ physicalSubjectRoot,
575
+ experiment.campaignSnapshot.subject.exclude,
576
+ );
577
+ if (outputStats && !options.force) {
578
+ throw new Error(`Output already exists (use --force): ${out}`);
579
+ }
580
+ await mkdir(dirname(out), { recursive: true });
581
+ const physicalParent = await realpath(dirname(out));
582
+ const finalPhysicalOut = resolve(physicalParent, basename(out));
583
+ assertReportDestination(
584
+ finalPhysicalOut,
585
+ physicalDirectory,
586
+ physicalSubjectRoot,
587
+ experiment.campaignSnapshot.subject.exclude,
588
+ );
589
+ await writeReportSafely(
590
+ finalPhysicalOut,
591
+ `${formatExperimentDecisionMarkdown(report)}\n`,
592
+ options.force ?? false,
593
+ );
594
+ return { path: out, report };
632
595
  };
633
596
 
634
- const caseIdOf = (casePath: string): string => basename(casePath, extname(casePath));
597
+ const compareCommand = async (
598
+ runA: string,
599
+ runB: string,
600
+ options: { passRateTolerance?: number | undefined },
601
+ ) => {
602
+ const pathA = resolve(runA);
603
+ const pathB = resolve(runB);
604
+ const invalid = (
605
+ await Promise.all(
606
+ [pathA, pathB].map(async (path) =>
607
+ existsSync(path) && (await stat(path)).isDirectory() ? undefined : path,
608
+ ),
609
+ )
610
+ ).filter((path): path is string => path !== undefined);
611
+ if (invalid.length > 0) {
612
+ throw new Errors.IncurError({
613
+ code: "RUN_DIRECTORY_NOT_FOUND",
614
+ message: invalid.map((path) => `${path} is not a directory`).join("; "),
615
+ exitCode: 1,
616
+ });
617
+ }
618
+ const [suiteA, suiteB] = await Promise.all([loadSuiteRun(pathA), loadSuiteRun(pathB)]);
619
+ if (Boolean(suiteA) !== Boolean(suiteB)) {
620
+ throw new Errors.IncurError({
621
+ code: "INCOMPATIBLE_RUN_TYPES",
622
+ message: "Both paths must be runs or both must be suites.",
623
+ exitCode: 1,
624
+ });
625
+ }
626
+ if (suiteA && suiteB) {
627
+ const comparison = await compareSuiteRuns(pathA, pathB, {
628
+ ...(options.passRateTolerance !== undefined
629
+ ? { passRateTolerance: options.passRateTolerance }
630
+ : {}),
631
+ });
632
+ if (hasSuiteRegression(comparison)) {
633
+ throw new Errors.IncurError({
634
+ code: "REGRESSION_DETECTED",
635
+ message: formatSuiteComparisonText(comparison),
636
+ exitCode: 1,
637
+ });
638
+ }
639
+ return comparison;
640
+ }
641
+ const comparison = await compareRuns({ pathA, pathB });
642
+ if (hasRegression(comparison)) {
643
+ throw new Errors.IncurError({
644
+ code: "REGRESSION_DETECTED",
645
+ message: formatComparisonText(comparison),
646
+ exitCode: 1,
647
+ });
648
+ }
649
+ return comparison;
650
+ };
635
651
 
636
652
  const SIGINT_EXIT_CODE = 130;
637
653
 
@@ -705,98 +721,22 @@ const isFailure = (outcome: { exitReason: string; casePassed: boolean | null }):
705
721
 
706
722
  const firstLine = (message: string): string => (message.split("\n")[0] ?? message).trim();
707
723
 
708
- const suiteTitle = (
709
- patterns: string[],
710
- tags: string[],
711
- explicit: SuiteItem[],
712
- loop: number,
713
- ): string => {
714
- const parts = [...patterns, ...explicit.map((e) => e.id), ...tags.map((t) => `#${t}`)];
715
- const filter = parts.length > 0 ? ` ${parts.join(" ")}` : "";
716
- const repeat = loop > 1 ? ` ×${loop}` : "";
717
- return `run${filter}${repeat}`;
718
- };
719
-
720
- type FilterPartition = { explicit: SuiteItem[]; patterns: string[] };
721
-
722
- // A filter that names an existing `.ts` file runs that file directly — it may
723
- // sit outside the discovery root. Everything else is a substring pattern — but
724
- // a `.ts` filter that resolves to nothing is almost always a typo'd path, so
725
- // the silent downgrade to a pattern gets called out.
726
- const partitionFilters = (filters: string[], cwd: string): FilterPartition => {
727
- const explicit: SuiteItem[] = [];
728
- const patterns: string[] = [];
729
- for (const f of filters) {
730
- const abs = resolve(cwd, f);
731
- if (f.endsWith(".ts") && existsSync(abs)) {
732
- explicit.push({ path: abs, id: caseIdOf(f) });
733
- } else {
734
- if (f.endsWith(".ts")) {
735
- process.stderr.write(`lux: ${abs} does not exist; treating "${f}" as a pattern\n`);
736
- }
737
- patterns.push(f);
738
- }
739
- }
740
- return { explicit, patterns };
741
- };
742
-
743
- // The selected set — substring/tag matches plus explicitly named files — is
744
- // the same whether it is run, `--list`ed, or `--list-tags`ed. No filters at
745
- // all means "everything discovered"; any filter narrows.
746
- const selectSuite = (
747
- cases: DiscoveredCase[],
748
- { explicit, patterns }: FilterPartition,
749
- tags: string[],
750
- ): DiscoveredCase[] => {
751
- const hasFilter = explicit.length > 0 || patterns.length > 0 || tags.length > 0;
752
- const byPath = new Map<string, DiscoveredCase>();
753
- if (!hasFilter) {
754
- for (const c of cases) byPath.set(c.path, c);
755
- } else {
756
- if (patterns.length > 0 || tags.length > 0) {
757
- for (const c of selectCases(cases, { patterns, tags })) byPath.set(c.path, c);
758
- }
759
- for (const e of explicit) {
760
- const discovered = cases.find((c) => c.path === e.path);
761
- // Discovery knows the case's declared id; the filename-derived one is
762
- // only a fallback for files discovery never saw.
763
- byPath.set(e.path, {
764
- path: e.path,
765
- id: discovered?.id ?? e.id,
766
- relPath: discovered?.relPath ?? e.id,
767
- tags: discovered?.tags ?? [],
768
- });
769
- }
770
- }
771
- return [...byPath.values()].sort((a, b) => a.id.localeCompare(b.id));
772
- };
773
-
774
- // `--loop` repeats each eval before moving to the next; each repeat gets a
775
- // `#n` display suffix so the reporter keeps them apart, while `caseId` keeps
776
- // every repeat filed under one run history.
777
- const expandLoop = (base: SuiteItem[], loop: number): SuiteItem[] =>
778
- loop > 1
779
- ? base.flatMap((b) =>
780
- Array.from({ length: loop }, (_, i) => ({
781
- path: b.path,
782
- id: `${b.id} #${i + 1}`,
783
- caseId: b.caseId ?? b.id,
784
- })),
785
- )
786
- : base;
787
-
788
724
  const experimentCommand = async (
789
725
  mode: "experiment" | "optimize",
790
726
  campaignPath: string,
791
- options: { runsRoot?: string; resume?: string; json?: boolean; plan?: boolean },
792
- ): Promise<void> => {
727
+ options: {
728
+ runsRoot?: string | undefined;
729
+ resume?: string | undefined;
730
+ plan?: boolean | undefined;
731
+ },
732
+ ) => {
793
733
  const cwd = process.cwd();
794
734
  const config = await loadLuxConfig(cwd);
795
735
  const campaign = await resolveExperimentCampaignPath(config.rootDir, campaignPath);
796
736
  const loaded = await loadExperimentCampaign(campaign);
797
737
  experimentCampaignMode(campaign, loaded.campaign, mode);
798
738
  const registry = buildRegistryFromConfig(config);
799
- const controller = abortOnInterrupt();
739
+ using controller = abortOnInterrupt();
800
740
  const execution = {
801
741
  config,
802
742
  registry,
@@ -807,64 +747,53 @@ const experimentCommand = async (
807
747
  };
808
748
  if (options.plan) {
809
749
  const plan = await planExperimentCampaign(execution, mode);
810
- let output: string;
811
- if (options.json) {
812
- output = `${JSON.stringify(plan, null, 2)}\n`;
813
- } else {
814
- const projectedStatus = plan.fitsProjectedCallBudgets ? "yes" : "no";
815
- output = [
816
- `campaign: ${plan.campaignId}`,
817
- `mode: ${plan.mode}`,
818
- `evaluation: ${plan.evaluationKind}`,
819
- `profiles: ${plan.profiles.join(", ")}`,
820
- `splits: ${plan.splits.map((split) => `${split.split}=${split.examples}`).join(", ")}`,
821
- `components: ${plan.mutableComponents.length}`,
822
- `candidates: ${plan.projected.candidates}`,
823
- `metric calls: ${plan.projected.metricCalls}/${plan.budget.maxMetricCalls}`,
824
- `judge calls: ${plan.projected.judgeCalls}/${plan.budget.maxJudgeCalls}`,
825
- `proposal calls:${plan.projected.proposalCalls}/${plan.budget.maxProposalCalls}`,
826
- `unpriced calls: observed at runtime (cap ${plan.budget.maxUnpricedModelCalls})`,
827
- `projected work within budget: ${projectedStatus}`,
828
- ...plan.warnings.map((warning) => `warning: ${warning}`),
829
- "",
830
- ].join("\n");
750
+ if (!plan.fitsProjectedCallBudgets) {
751
+ throw new Errors.IncurError({
752
+ code: "EXPERIMENT_BUDGET_EXCEEDED",
753
+ message: "The projected experiment work exceeds its configured call budgets.",
754
+ exitCode: 1,
755
+ });
831
756
  }
832
- process.stdout.write(output);
833
- if (!plan.fitsProjectedCallBudgets) failWith(1);
834
- return;
757
+ return plan;
835
758
  }
836
759
  const result =
837
760
  mode === "optimize"
838
761
  ? await optimizeExperimentCampaign(execution)
839
762
  : await runExperimentCampaign(execution);
840
- process.stdout.write(
841
- options.json
842
- ? `${JSON.stringify({ experimentDir: result.experimentDir, ...result.manifest }, null, 2)}\n`
843
- : `${formatExperimentRun(result.experimentDir, result.manifest)}\n`,
844
- );
845
763
  if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
846
- else if (result.manifest.status !== "passed") failWith(1);
764
+ if (result.manifest.status !== "passed") {
765
+ throw new Errors.IncurError({
766
+ code: "EXPERIMENT_FAILED",
767
+ message: `Experiment ${result.manifest.experimentId} finished with status '${result.manifest.status}'.`,
768
+ exitCode: 1,
769
+ });
770
+ }
771
+ return { experimentDir: result.experimentDir, ...result.manifest };
847
772
  };
848
773
 
849
- const runCommand = async (filters: string[], options: RunOptions): Promise<void> => {
774
+ /** A profile pins one driver, so callers pass the project matrix only without one. */
775
+ const runCommand = async (filters: string[], options: RunOptions, human = false) => {
850
776
  if (options.interactive && options.claudeAnswerer) {
851
- throw new Error("lux run: --interactive and --claude-answerer are mutually exclusive");
777
+ throw new Errors.ParseError({
778
+ message: "--interactive and --claude-answerer are mutually exclusive",
779
+ });
852
780
  }
853
781
  const cwd = process.cwd();
854
782
  const config = await loadLuxConfig(cwd);
855
783
  const profile = options.profile ? config.profiles?.[options.profile] : undefined;
856
784
  if (options.profile && !profile) {
857
785
  const available = Object.keys(config.profiles ?? {}).sort();
858
- process.stderr.write(
859
- `lux: unknown profile '${options.profile}'${available.length > 0 ? `; available: ${available.join(", ")}` : ""}\n`,
860
- );
861
- failWith(1);
862
- return;
786
+ throw new Errors.IncurError({
787
+ code: "PROFILE_NOT_FOUND",
788
+ message: `Unknown profile '${options.profile}'${available.length > 0 ? `; available: ${available.join(", ")}` : ""}.`,
789
+ exitCode: 1,
790
+ });
863
791
  }
864
792
  const harness = profile?.harness ?? config.harness;
865
- const registry = buildRegistryFromConfig(config).withAssertion(createRubricAssertion(harness));
793
+ const registry = registryWithHarness(buildRegistryFromConfig(config), config, harness);
866
794
  const defaultDriver = profile?.driver ?? config.defaultDriver;
867
795
  const orchestrator = new Orchestrator({
796
+ environment: profile?.environment ?? config.environment,
868
797
  registry,
869
798
  defaultAnswerer: profile?.answerer ?? config.defaultAnswerer,
870
799
  ...(defaultDriver ? { defaultDriver } : {}),
@@ -882,23 +811,26 @@ const runCommand = async (filters: string[], options: RunOptions): Promise<void>
882
811
  // broken config, not an empty suite — never mask it behind "no cases
883
812
  // matched" or a lucky explicit-file run.
884
813
  if (config.casesRoot && !existsSync(config.casesRoot)) {
885
- process.stderr.write(`lux: casesRoot does not exist: ${config.casesRoot}\n`);
886
- failWith(1);
887
- return;
814
+ throw new Errors.IncurError({
815
+ code: "CASES_ROOT_NOT_FOUND",
816
+ message: `casesRoot does not exist: ${config.casesRoot}`,
817
+ exitCode: 1,
818
+ });
888
819
  }
889
820
 
890
821
  const { explicit, patterns } = partitionFilters(filters, cwd);
891
- const tags = options.tag ?? [];
822
+ const tags = normalizeTags(options.tag ?? []);
892
823
  const hasFilter = explicit.length > 0 || patterns.length > 0 || tags.length > 0;
893
824
 
894
825
  // Explicitly named files need no discovery, so they run even without a
895
826
  // project; anything else would need a tree walk with nowhere safe to walk.
896
827
  if (config.configPath === null && !existsSync(root) && explicit.length === 0) {
897
- process.stderr.write(
898
- `lux: no lux.config.ts or cases/ directory in ${cwd} — run \`lux init\` to scaffold a project, or name a case file directly\n`,
899
- );
900
- failWith(1);
901
- return;
828
+ throw new Errors.IncurError({
829
+ code: "PROJECT_NOT_INITIALIZED",
830
+ message: `No lux.config.ts or cases/ directory in ${cwd}.`,
831
+ hint: "Run `lux init` or name a case file directly.",
832
+ exitCode: 1,
833
+ });
902
834
  }
903
835
 
904
836
  const { cases, problems } = existsSync(root)
@@ -913,35 +845,59 @@ const runCommand = async (filters: string[], options: RunOptions): Promise<void>
913
845
  // An empty selection fails every mode the same way — running, `--list`, and
914
846
  // `--list-tags` all exit 1, so a typo'd filter can never read as a clean pass.
915
847
  if (selected.length === 0) {
916
- process.stderr.write(
917
- hasFilter
918
- ? "lux: no cases matched the given filters\n"
919
- : `lux: no cases discovered under ${root}\n`,
920
- );
921
- failWith(1);
922
- return;
848
+ throw new Errors.IncurError({
849
+ code: "NO_CASES_MATCHED",
850
+ message: hasFilter
851
+ ? "No cases matched the given filters."
852
+ : `No cases were discovered under ${root}.`,
853
+ exitCode: 1,
854
+ });
923
855
  }
924
856
 
925
857
  // List the tag vocabulary of the selected cases — "which --tag values exist?".
926
858
  if (options.listTags) {
927
859
  const counts = new Map<string, number>();
928
860
  for (const c of selected) for (const t of c.tags) counts.set(t, (counts.get(t) ?? 0) + 1);
929
- for (const [tag, n] of [...counts].sort((a, b) => a[0].localeCompare(b[0]))) {
930
- process.stdout.write(`${tag}\t${n}\n`);
931
- }
932
- process.stderr.write(
933
- `lux: ${counts.size} tag${counts.size === 1 ? "" : "s"} across ${selected.length} cases\n`,
934
- );
935
- return;
861
+ const result = {
862
+ caseCount: selected.length,
863
+ tags: [...counts]
864
+ .sort((a, b) => a[0].localeCompare(b[0]))
865
+ .map(([tag, count]) => ({ tag, count })),
866
+ };
867
+ if (human)
868
+ process.stdout.write(
869
+ `${result.caseCount} cases\n${result.tags.map(({ tag, count }) => ` ${tag}: ${count}`).join("\n")}\n`,
870
+ );
871
+ return result;
936
872
  }
937
873
 
938
- const base: SuiteItem[] = selected.map((c) => ({ path: c.path, id: c.id }));
874
+ const focus: HarnessFocus = {
875
+ driver: options.driver,
876
+ model: options.model,
877
+ effort: options.effort,
878
+ };
879
+ const base = await planSuiteItems(selected, {
880
+ matrix: profile ? undefined : config.matrix,
881
+ defaultDriver,
882
+ registry,
883
+ focus,
884
+ });
939
885
 
940
886
  // Collect-only: answer "what would run?" without spawning a single agent.
941
887
  if (options.list) {
942
- for (const item of base) process.stdout.write(`${item.id}\n`);
943
- process.stderr.write(`lux: ${base.length} case${base.length === 1 ? "" : "s"} matched\n`);
944
- return;
888
+ if (human)
889
+ process.stdout.write(
890
+ `${base.length} cases\n${base.map(({ id, path }) => ` ${id} ${path}`).join("\n")}\n`,
891
+ );
892
+ return {
893
+ caseCount: base.length,
894
+ cases: base.map(({ path, id, caseId, permutationId }) => ({
895
+ path,
896
+ id,
897
+ ...(caseId !== undefined ? { caseId } : {}),
898
+ ...(permutationId !== undefined ? { permutationId } : {}),
899
+ })),
900
+ };
945
901
  }
946
902
 
947
903
  const loop = options.loop ?? 1;
@@ -950,10 +906,10 @@ const runCommand = async (filters: string[], options: RunOptions): Promise<void>
950
906
  const { concurrency, note } = resolveConcurrency(options, config.concurrency);
951
907
  if (note) process.stderr.write(note);
952
908
 
953
- const reporter = new ConsoleReporter({ verbose: options.verbose ?? false });
954
- const controller = abortOnInterrupt();
955
- reporter.begin({
956
- title: suiteTitle(patterns, tags, explicit, loop),
909
+ const reporter = human ? new ConsoleReporter({ verbose: options.verbose ?? false }) : undefined;
910
+ using controller = abortOnInterrupt();
911
+ reporter?.begin({
912
+ title: suiteTitle(patterns, tags, explicit, loop, focus),
957
913
  caseIds: items.map((i) => i.id),
958
914
  runsRoot,
959
915
  loop,
@@ -965,28 +921,40 @@ const runCommand = async (filters: string[], options: RunOptions): Promise<void>
965
921
  runsRoot,
966
922
  fixturesRoot: config.fixturesRoot,
967
923
  answererOverride: resolveAnswererOverride(options),
968
- observer: reporter,
924
+ skipJudge: options.skipJudge,
925
+ ...(reporter ? { observer: reporter } : {}),
969
926
  abortSignal: controller.signal,
970
927
  concurrency,
971
928
  subjectRoot,
972
929
  profileId: options.profile,
973
930
  });
974
931
  results = suite.results;
975
- reporter.setSuiteDir(suite.suiteDir);
932
+ reporter?.setSuiteDir(suite.suiteDir);
933
+ if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
934
+ if (results.some(isFailure)) {
935
+ throw new Errors.IncurError({
936
+ code: "SUITE_FAILED",
937
+ message: `${results.filter(isFailure).length} of ${results.length} case attempts failed. Suite: ${suite.suiteDir}`,
938
+ exitCode: 1,
939
+ });
940
+ }
941
+ return {
942
+ suiteDir: suite.suiteDir,
943
+ results: results.map((result) => ({
944
+ caseId: result.caseId,
945
+ runDir: result.runDir ?? null,
946
+ exitReason: result.exitReason,
947
+ casePassed: result.casePassed,
948
+ caseScore: result.result?.grading?.caseScore ?? null,
949
+ assertionsPassed:
950
+ result.result?.grading?.assertions.filter((assertion) => assertion.passed).length ?? 0,
951
+ assertionsTotal: result.result?.grading?.assertions.length ?? 0,
952
+ })),
953
+ };
976
954
  } finally {
977
- reporter.finish();
955
+ reporter?.finish();
978
956
  }
979
-
980
- if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
981
- else if (results.some(isFailure)) failWith(1);
982
957
  };
983
958
 
984
959
  // Internal exports for testing
985
- export {
986
- expandLoop,
987
- isFailure,
988
- parsePositiveInt,
989
- partitionFilters,
990
- resolveConcurrency,
991
- selectSuite,
992
- };
960
+ export { isFailure, resolveConcurrency };