@prismatic-io/lux 0.0.1

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 (636) hide show
  1. package/LICENSE +21 -0
  2. package/lib/answerers/claude-code/index.d.ts +17 -0
  3. package/lib/answerers/claude-code/index.d.ts.map +1 -0
  4. package/lib/answerers/claude-code/index.js +280 -0
  5. package/lib/answerers/claude-code/index.js.map +1 -0
  6. package/lib/answerers/persona/index.d.ts +40 -0
  7. package/lib/answerers/persona/index.d.ts.map +1 -0
  8. package/lib/answerers/persona/index.js +265 -0
  9. package/lib/answerers/persona/index.js.map +1 -0
  10. package/lib/answerers/scripted/index.d.ts +8 -0
  11. package/lib/answerers/scripted/index.d.ts.map +1 -0
  12. package/lib/answerers/scripted/index.js +163 -0
  13. package/lib/answerers/scripted/index.js.map +1 -0
  14. package/lib/answerers/shared.d.ts +13 -0
  15. package/lib/answerers/shared.d.ts.map +1 -0
  16. package/lib/answerers/shared.js +17 -0
  17. package/lib/answerers/shared.js.map +1 -0
  18. package/lib/answerers/terminal/index.d.ts +9 -0
  19. package/lib/answerers/terminal/index.d.ts.map +1 -0
  20. package/lib/answerers/terminal/index.js +97 -0
  21. package/lib/answerers/terminal/index.js.map +1 -0
  22. package/lib/assertions/core/command-exits-zero.d.ts +21 -0
  23. package/lib/assertions/core/command-exits-zero.d.ts.map +1 -0
  24. package/lib/assertions/core/command-exits-zero.js +87 -0
  25. package/lib/assertions/core/command-exits-zero.js.map +1 -0
  26. package/lib/assertions/core/event-checks.d.ts +57 -0
  27. package/lib/assertions/core/event-checks.d.ts.map +1 -0
  28. package/lib/assertions/core/event-checks.js +123 -0
  29. package/lib/assertions/core/event-checks.js.map +1 -0
  30. package/lib/assertions/core/file-checks.d.ts +103 -0
  31. package/lib/assertions/core/file-checks.d.ts.map +1 -0
  32. package/lib/assertions/core/file-checks.js +288 -0
  33. package/lib/assertions/core/file-checks.js.map +1 -0
  34. package/lib/assertions/core/helpers.d.ts +66 -0
  35. package/lib/assertions/core/helpers.d.ts.map +1 -0
  36. package/lib/assertions/core/helpers.js +170 -0
  37. package/lib/assertions/core/helpers.js.map +1 -0
  38. package/lib/assertions/core/index.d.ts +15 -0
  39. package/lib/assertions/core/index.d.ts.map +1 -0
  40. package/lib/assertions/core/index.js +56 -0
  41. package/lib/assertions/core/index.js.map +1 -0
  42. package/lib/assertions/core/json-pointer-equals.d.ts +19 -0
  43. package/lib/assertions/core/json-pointer-equals.d.ts.map +1 -0
  44. package/lib/assertions/core/json-pointer-equals.js +71 -0
  45. package/lib/assertions/core/json-pointer-equals.js.map +1 -0
  46. package/lib/assertions/core/output-checks.d.ts +46 -0
  47. package/lib/assertions/core/output-checks.d.ts.map +1 -0
  48. package/lib/assertions/core/output-checks.js +121 -0
  49. package/lib/assertions/core/output-checks.js.map +1 -0
  50. package/lib/assertions/core/predicate.d.ts +14 -0
  51. package/lib/assertions/core/predicate.d.ts.map +1 -0
  52. package/lib/assertions/core/predicate.js +55 -0
  53. package/lib/assertions/core/predicate.js.map +1 -0
  54. package/lib/assertions/core/resource-checks.d.ts +71 -0
  55. package/lib/assertions/core/resource-checks.d.ts.map +1 -0
  56. package/lib/assertions/core/resource-checks.js +159 -0
  57. package/lib/assertions/core/resource-checks.js.map +1 -0
  58. package/lib/assertions/core/safe-regex.d.ts +23 -0
  59. package/lib/assertions/core/safe-regex.d.ts.map +1 -0
  60. package/lib/assertions/core/safe-regex.js +102 -0
  61. package/lib/assertions/core/safe-regex.js.map +1 -0
  62. package/lib/assertions/core/tool-called-with.d.ts +21 -0
  63. package/lib/assertions/core/tool-called-with.d.ts.map +1 -0
  64. package/lib/assertions/core/tool-called-with.js +69 -0
  65. package/lib/assertions/core/tool-called-with.js.map +1 -0
  66. package/lib/assertions/core/tool-calls.d.ts +22 -0
  67. package/lib/assertions/core/tool-calls.d.ts.map +1 -0
  68. package/lib/assertions/core/tool-calls.js +17 -0
  69. package/lib/assertions/core/tool-calls.js.map +1 -0
  70. package/lib/assertions/core/tool-invocation.d.ts +36 -0
  71. package/lib/assertions/core/tool-invocation.d.ts.map +1 -0
  72. package/lib/assertions/core/tool-invocation.js +170 -0
  73. package/lib/assertions/core/tool-invocation.js.map +1 -0
  74. package/lib/assertions/core/tool-result-checks.d.ts +59 -0
  75. package/lib/assertions/core/tool-result-checks.d.ts.map +1 -0
  76. package/lib/assertions/core/tool-result-checks.js +193 -0
  77. package/lib/assertions/core/tool-result-checks.js.map +1 -0
  78. package/lib/assertions/mcp/discovery-checks.d.ts +36 -0
  79. package/lib/assertions/mcp/discovery-checks.d.ts.map +1 -0
  80. package/lib/assertions/mcp/discovery-checks.js +122 -0
  81. package/lib/assertions/mcp/discovery-checks.js.map +1 -0
  82. package/lib/assertions/mcp/index.d.ts +10 -0
  83. package/lib/assertions/mcp/index.d.ts.map +1 -0
  84. package/lib/assertions/mcp/index.js +25 -0
  85. package/lib/assertions/mcp/index.js.map +1 -0
  86. package/lib/assertions/rubric/index.d.ts +104 -0
  87. package/lib/assertions/rubric/index.d.ts.map +1 -0
  88. package/lib/assertions/rubric/index.js +191 -0
  89. package/lib/assertions/rubric/index.js.map +1 -0
  90. package/lib/assertions/rubric/internal.d.ts +14 -0
  91. package/lib/assertions/rubric/internal.d.ts.map +1 -0
  92. package/lib/assertions/rubric/internal.js +335 -0
  93. package/lib/assertions/rubric/internal.js.map +1 -0
  94. package/lib/authoring.d.ts +3 -0
  95. package/lib/authoring.d.ts.map +1 -0
  96. package/lib/authoring.js +3 -0
  97. package/lib/authoring.js.map +1 -0
  98. package/lib/cli/bin.d.ts +3 -0
  99. package/lib/cli/bin.d.ts.map +1 -0
  100. package/lib/cli/bin.js +15 -0
  101. package/lib/cli/bin.js.map +1 -0
  102. package/lib/cli/command-runtime.d.ts +5 -0
  103. package/lib/cli/command-runtime.d.ts.map +1 -0
  104. package/lib/cli/command-runtime.js +30 -0
  105. package/lib/cli/command-runtime.js.map +1 -0
  106. package/lib/cli/init.d.ts +21 -0
  107. package/lib/cli/init.d.ts.map +1 -0
  108. package/lib/cli/init.js +340 -0
  109. package/lib/cli/init.js.map +1 -0
  110. package/lib/cli/program.d.ts +22 -0
  111. package/lib/cli/program.d.ts.map +1 -0
  112. package/lib/cli/program.js +757 -0
  113. package/lib/cli/program.js.map +1 -0
  114. package/lib/cli/render/format.d.ts +14 -0
  115. package/lib/cli/render/format.d.ts.map +1 -0
  116. package/lib/cli/render/format.js +71 -0
  117. package/lib/cli/render/format.js.map +1 -0
  118. package/lib/cli/render/reporter.d.ts +73 -0
  119. package/lib/cli/render/reporter.d.ts.map +1 -0
  120. package/lib/cli/render/reporter.js +519 -0
  121. package/lib/cli/render/reporter.js.map +1 -0
  122. package/lib/cli/render/tty.d.ts +77 -0
  123. package/lib/cli/render/tty.d.ts.map +1 -0
  124. package/lib/cli/render/tty.js +283 -0
  125. package/lib/cli/render/tty.js.map +1 -0
  126. package/lib/cli/run-options.d.ts +27 -0
  127. package/lib/cli/run-options.d.ts.map +1 -0
  128. package/lib/cli/run-options.js +58 -0
  129. package/lib/cli/run-options.js.map +1 -0
  130. package/lib/cli/skills.d.ts +38 -0
  131. package/lib/cli/skills.d.ts.map +1 -0
  132. package/lib/cli/skills.js +84 -0
  133. package/lib/cli/skills.js.map +1 -0
  134. package/lib/core/annotation.d.ts +56 -0
  135. package/lib/core/annotation.d.ts.map +1 -0
  136. package/lib/core/annotation.js +92 -0
  137. package/lib/core/annotation.js.map +1 -0
  138. package/lib/core/answerer.d.ts +68 -0
  139. package/lib/core/answerer.d.ts.map +1 -0
  140. package/lib/core/answerer.js +17 -0
  141. package/lib/core/answerer.js.map +1 -0
  142. package/lib/core/artifact-evidence.d.ts +50 -0
  143. package/lib/core/artifact-evidence.d.ts.map +1 -0
  144. package/lib/core/artifact-evidence.js +185 -0
  145. package/lib/core/artifact-evidence.js.map +1 -0
  146. package/lib/core/artifact-snapshot.d.ts +37 -0
  147. package/lib/core/artifact-snapshot.d.ts.map +1 -0
  148. package/lib/core/artifact-snapshot.js +339 -0
  149. package/lib/core/artifact-snapshot.js.map +1 -0
  150. package/lib/core/assertion.d.ts +129 -0
  151. package/lib/core/assertion.d.ts.map +1 -0
  152. package/lib/core/assertion.js +75 -0
  153. package/lib/core/assertion.js.map +1 -0
  154. package/lib/core/case.d.ts +79 -0
  155. package/lib/core/case.d.ts.map +1 -0
  156. package/lib/core/case.js +90 -0
  157. package/lib/core/case.js.map +1 -0
  158. package/lib/core/conversation-turn.d.ts +23 -0
  159. package/lib/core/conversation-turn.d.ts.map +1 -0
  160. package/lib/core/conversation-turn.js +31 -0
  161. package/lib/core/conversation-turn.js.map +1 -0
  162. package/lib/core/driver.d.ts +64 -0
  163. package/lib/core/driver.d.ts.map +1 -0
  164. package/lib/core/driver.js +18 -0
  165. package/lib/core/driver.js.map +1 -0
  166. package/lib/core/emitter.d.ts +20 -0
  167. package/lib/core/emitter.d.ts.map +1 -0
  168. package/lib/core/emitter.js +34 -0
  169. package/lib/core/emitter.js.map +1 -0
  170. package/lib/core/events.d.ts +202 -0
  171. package/lib/core/events.d.ts.map +1 -0
  172. package/lib/core/events.js +107 -0
  173. package/lib/core/events.js.map +1 -0
  174. package/lib/core/exec.d.ts +82 -0
  175. package/lib/core/exec.d.ts.map +1 -0
  176. package/lib/core/exec.js +168 -0
  177. package/lib/core/exec.js.map +1 -0
  178. package/lib/core/experiment.d.ts +285 -0
  179. package/lib/core/experiment.d.ts.map +1 -0
  180. package/lib/core/experiment.js +389 -0
  181. package/lib/core/experiment.js.map +1 -0
  182. package/lib/core/glob.d.ts +3 -0
  183. package/lib/core/glob.d.ts.map +1 -0
  184. package/lib/core/glob.js +32 -0
  185. package/lib/core/glob.js.map +1 -0
  186. package/lib/core/hash.d.ts +8 -0
  187. package/lib/core/hash.d.ts.map +1 -0
  188. package/lib/core/hash.js +214 -0
  189. package/lib/core/hash.js.map +1 -0
  190. package/lib/core/index.d.ts +29 -0
  191. package/lib/core/index.d.ts.map +1 -0
  192. package/lib/core/index.js +29 -0
  193. package/lib/core/index.js.map +1 -0
  194. package/lib/core/lifecycle-fixtures.d.ts +20 -0
  195. package/lib/core/lifecycle-fixtures.d.ts.map +1 -0
  196. package/lib/core/lifecycle-fixtures.js +109 -0
  197. package/lib/core/lifecycle-fixtures.js.map +1 -0
  198. package/lib/core/lifecycle-hooks.d.ts +52 -0
  199. package/lib/core/lifecycle-hooks.d.ts.map +1 -0
  200. package/lib/core/lifecycle-hooks.js +42 -0
  201. package/lib/core/lifecycle-hooks.js.map +1 -0
  202. package/lib/core/mcp-events.d.ts +44 -0
  203. package/lib/core/mcp-events.d.ts.map +1 -0
  204. package/lib/core/mcp-events.js +23 -0
  205. package/lib/core/mcp-events.js.map +1 -0
  206. package/lib/core/model-cli.d.ts +57 -0
  207. package/lib/core/model-cli.d.ts.map +1 -0
  208. package/lib/core/model-cli.js +237 -0
  209. package/lib/core/model-cli.js.map +1 -0
  210. package/lib/core/observer.d.ts +31 -0
  211. package/lib/core/observer.d.ts.map +1 -0
  212. package/lib/core/observer.js +25 -0
  213. package/lib/core/observer.js.map +1 -0
  214. package/lib/core/platform-process.d.ts +33 -0
  215. package/lib/core/platform-process.d.ts.map +1 -0
  216. package/lib/core/platform-process.js +214 -0
  217. package/lib/core/platform-process.js.map +1 -0
  218. package/lib/core/registry.d.ts +22 -0
  219. package/lib/core/registry.d.ts.map +1 -0
  220. package/lib/core/registry.js +45 -0
  221. package/lib/core/registry.js.map +1 -0
  222. package/lib/core/rubric.d.ts +18 -0
  223. package/lib/core/rubric.d.ts.map +1 -0
  224. package/lib/core/rubric.js +49 -0
  225. package/lib/core/rubric.js.map +1 -0
  226. package/lib/core/run.d.ts +419 -0
  227. package/lib/core/run.d.ts.map +1 -0
  228. package/lib/core/run.js +126 -0
  229. package/lib/core/run.js.map +1 -0
  230. package/lib/core/statistics.d.ts +39 -0
  231. package/lib/core/statistics.d.ts.map +1 -0
  232. package/lib/core/statistics.js +163 -0
  233. package/lib/core/statistics.js.map +1 -0
  234. package/lib/core/subject.d.ts +4 -0
  235. package/lib/core/subject.d.ts.map +1 -0
  236. package/lib/core/subject.js +46 -0
  237. package/lib/core/subject.js.map +1 -0
  238. package/lib/core/tool-events.d.ts +69 -0
  239. package/lib/core/tool-events.d.ts.map +1 -0
  240. package/lib/core/tool-events.js +242 -0
  241. package/lib/core/tool-events.js.map +1 -0
  242. package/lib/core/usage.d.ts +78 -0
  243. package/lib/core/usage.d.ts.map +1 -0
  244. package/lib/core/usage.js +104 -0
  245. package/lib/core/usage.js.map +1 -0
  246. package/lib/core/wire.d.ts +83 -0
  247. package/lib/core/wire.d.ts.map +1 -0
  248. package/lib/core/wire.js +101 -0
  249. package/lib/core/wire.js.map +1 -0
  250. package/lib/core/zod.d.ts +10 -0
  251. package/lib/core/zod.d.ts.map +1 -0
  252. package/lib/core/zod.js +18 -0
  253. package/lib/core/zod.js.map +1 -0
  254. package/lib/drivers/claude-code/index.d.ts +55 -0
  255. package/lib/drivers/claude-code/index.d.ts.map +1 -0
  256. package/lib/drivers/claude-code/index.js +779 -0
  257. package/lib/drivers/claude-code/index.js.map +1 -0
  258. package/lib/drivers/claude-code/parse-events.d.ts +46 -0
  259. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -0
  260. package/lib/drivers/claude-code/parse-events.js +387 -0
  261. package/lib/drivers/claude-code/parse-events.js.map +1 -0
  262. package/lib/drivers/codex/app-events.d.ts +16 -0
  263. package/lib/drivers/codex/app-events.d.ts.map +1 -0
  264. package/lib/drivers/codex/app-events.js +184 -0
  265. package/lib/drivers/codex/app-events.js.map +1 -0
  266. package/lib/drivers/codex/config.d.ts +55 -0
  267. package/lib/drivers/codex/config.d.ts.map +1 -0
  268. package/lib/drivers/codex/config.js +125 -0
  269. package/lib/drivers/codex/config.js.map +1 -0
  270. package/lib/drivers/codex/index.d.ts +8 -0
  271. package/lib/drivers/codex/index.d.ts.map +1 -0
  272. package/lib/drivers/codex/index.js +904 -0
  273. package/lib/drivers/codex/index.js.map +1 -0
  274. package/lib/drivers/mcp/index.d.ts +52 -0
  275. package/lib/drivers/mcp/index.d.ts.map +1 -0
  276. package/lib/drivers/mcp/index.js +282 -0
  277. package/lib/drivers/mcp/index.js.map +1 -0
  278. package/lib/drivers/mcp/transport.d.ts +50 -0
  279. package/lib/drivers/mcp/transport.d.ts.map +1 -0
  280. package/lib/drivers/mcp/transport.js +190 -0
  281. package/lib/drivers/mcp/transport.js.map +1 -0
  282. package/lib/drivers/shared/artifacts.d.ts +4 -0
  283. package/lib/drivers/shared/artifacts.d.ts.map +1 -0
  284. package/lib/drivers/shared/artifacts.js +81 -0
  285. package/lib/drivers/shared/artifacts.js.map +1 -0
  286. package/lib/drivers/shared/secure-copy.d.ts +2 -0
  287. package/lib/drivers/shared/secure-copy.d.ts.map +1 -0
  288. package/lib/drivers/shared/secure-copy.js +35 -0
  289. package/lib/drivers/shared/secure-copy.js.map +1 -0
  290. package/lib/drivers/subprocess/index.d.ts +24 -0
  291. package/lib/drivers/subprocess/index.d.ts.map +1 -0
  292. package/lib/drivers/subprocess/index.js +451 -0
  293. package/lib/drivers/subprocess/index.js.map +1 -0
  294. package/lib/index.d.ts +40 -0
  295. package/lib/index.d.ts.map +1 -0
  296. package/lib/index.js +48 -0
  297. package/lib/index.js.map +1 -0
  298. package/lib/optimization/reflective.d.ts +27 -0
  299. package/lib/optimization/reflective.d.ts.map +1 -0
  300. package/lib/optimization/reflective.js +257 -0
  301. package/lib/optimization/reflective.js.map +1 -0
  302. package/lib/orchestrator/annotation-loader.d.ts +65 -0
  303. package/lib/orchestrator/annotation-loader.d.ts.map +1 -0
  304. package/lib/orchestrator/annotation-loader.js +73 -0
  305. package/lib/orchestrator/annotation-loader.js.map +1 -0
  306. package/lib/orchestrator/annotation-store.d.ts +3 -0
  307. package/lib/orchestrator/annotation-store.d.ts.map +1 -0
  308. package/lib/orchestrator/annotation-store.js +100 -0
  309. package/lib/orchestrator/annotation-store.js.map +1 -0
  310. package/lib/orchestrator/authored-dependencies.d.ts +3 -0
  311. package/lib/orchestrator/authored-dependencies.d.ts.map +1 -0
  312. package/lib/orchestrator/authored-dependencies.js +146 -0
  313. package/lib/orchestrator/authored-dependencies.js.map +1 -0
  314. package/lib/orchestrator/campaign-lifecycle.d.ts +105 -0
  315. package/lib/orchestrator/campaign-lifecycle.d.ts.map +1 -0
  316. package/lib/orchestrator/campaign-lifecycle.js +141 -0
  317. package/lib/orchestrator/campaign-lifecycle.js.map +1 -0
  318. package/lib/orchestrator/candidate-integrity.d.ts +4 -0
  319. package/lib/orchestrator/candidate-integrity.d.ts.map +1 -0
  320. package/lib/orchestrator/candidate-integrity.js +14 -0
  321. package/lib/orchestrator/candidate-integrity.js.map +1 -0
  322. package/lib/orchestrator/candidates.d.ts +13 -0
  323. package/lib/orchestrator/candidates.d.ts.map +1 -0
  324. package/lib/orchestrator/candidates.js +577 -0
  325. package/lib/orchestrator/candidates.js.map +1 -0
  326. package/lib/orchestrator/compare.d.ts +80 -0
  327. package/lib/orchestrator/compare.d.ts.map +1 -0
  328. package/lib/orchestrator/compare.js +270 -0
  329. package/lib/orchestrator/compare.js.map +1 -0
  330. package/lib/orchestrator/comparison-identity.d.ts +10 -0
  331. package/lib/orchestrator/comparison-identity.d.ts.map +1 -0
  332. package/lib/orchestrator/comparison-identity.js +39 -0
  333. package/lib/orchestrator/comparison-identity.js.map +1 -0
  334. package/lib/orchestrator/config.d.ts +121 -0
  335. package/lib/orchestrator/config.d.ts.map +1 -0
  336. package/lib/orchestrator/config.js +176 -0
  337. package/lib/orchestrator/config.js.map +1 -0
  338. package/lib/orchestrator/content-bound-json.d.ts +5 -0
  339. package/lib/orchestrator/content-bound-json.d.ts.map +1 -0
  340. package/lib/orchestrator/content-bound-json.js +59 -0
  341. package/lib/orchestrator/content-bound-json.js.map +1 -0
  342. package/lib/orchestrator/discover.d.ts +45 -0
  343. package/lib/orchestrator/discover.d.ts.map +1 -0
  344. package/lib/orchestrator/discover.js +98 -0
  345. package/lib/orchestrator/discover.js.map +1 -0
  346. package/lib/orchestrator/doctor.d.ts +141 -0
  347. package/lib/orchestrator/doctor.d.ts.map +1 -0
  348. package/lib/orchestrator/doctor.js +525 -0
  349. package/lib/orchestrator/doctor.js.map +1 -0
  350. package/lib/orchestrator/driver-session.d.ts +177 -0
  351. package/lib/orchestrator/driver-session.d.ts.map +1 -0
  352. package/lib/orchestrator/driver-session.js +396 -0
  353. package/lib/orchestrator/driver-session.js.map +1 -0
  354. package/lib/orchestrator/evaluator-identity.d.ts +7 -0
  355. package/lib/orchestrator/evaluator-identity.d.ts.map +1 -0
  356. package/lib/orchestrator/evaluator-identity.js +42 -0
  357. package/lib/orchestrator/evaluator-identity.js.map +1 -0
  358. package/lib/orchestrator/experiment-budget.d.ts +11 -0
  359. package/lib/orchestrator/experiment-budget.d.ts.map +1 -0
  360. package/lib/orchestrator/experiment-budget.js +51 -0
  361. package/lib/orchestrator/experiment-budget.js.map +1 -0
  362. package/lib/orchestrator/experiment-context.d.ts +43 -0
  363. package/lib/orchestrator/experiment-context.d.ts.map +1 -0
  364. package/lib/orchestrator/experiment-context.js +9 -0
  365. package/lib/orchestrator/experiment-context.js.map +1 -0
  366. package/lib/orchestrator/experiment-contracts.d.ts +290 -0
  367. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -0
  368. package/lib/orchestrator/experiment-contracts.js +38 -0
  369. package/lib/orchestrator/experiment-contracts.js.map +1 -0
  370. package/lib/orchestrator/experiment-corpus.d.ts +21 -0
  371. package/lib/orchestrator/experiment-corpus.d.ts.map +1 -0
  372. package/lib/orchestrator/experiment-corpus.js +276 -0
  373. package/lib/orchestrator/experiment-corpus.js.map +1 -0
  374. package/lib/orchestrator/experiment-evaluation.d.ts +44 -0
  375. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -0
  376. package/lib/orchestrator/experiment-evaluation.js +542 -0
  377. package/lib/orchestrator/experiment-evaluation.js.map +1 -0
  378. package/lib/orchestrator/experiment-finish.d.ts +6 -0
  379. package/lib/orchestrator/experiment-finish.d.ts.map +1 -0
  380. package/lib/orchestrator/experiment-finish.js +19 -0
  381. package/lib/orchestrator/experiment-finish.js.map +1 -0
  382. package/lib/orchestrator/experiment-identity.d.ts +6 -0
  383. package/lib/orchestrator/experiment-identity.d.ts.map +1 -0
  384. package/lib/orchestrator/experiment-identity.js +257 -0
  385. package/lib/orchestrator/experiment-identity.js.map +1 -0
  386. package/lib/orchestrator/experiment-loader.d.ts +175 -0
  387. package/lib/orchestrator/experiment-loader.d.ts.map +1 -0
  388. package/lib/orchestrator/experiment-loader.js +77 -0
  389. package/lib/orchestrator/experiment-loader.js.map +1 -0
  390. package/lib/orchestrator/experiment-report.d.ts +450 -0
  391. package/lib/orchestrator/experiment-report.d.ts.map +1 -0
  392. package/lib/orchestrator/experiment-report.js +570 -0
  393. package/lib/orchestrator/experiment-report.js.map +1 -0
  394. package/lib/orchestrator/experiment-runs.d.ts +379 -0
  395. package/lib/orchestrator/experiment-runs.d.ts.map +1 -0
  396. package/lib/orchestrator/experiment-runs.js +796 -0
  397. package/lib/orchestrator/experiment-runs.js.map +1 -0
  398. package/lib/orchestrator/experiment-runtime-identity.d.ts +41 -0
  399. package/lib/orchestrator/experiment-runtime-identity.d.ts.map +1 -0
  400. package/lib/orchestrator/experiment-runtime-identity.js +411 -0
  401. package/lib/orchestrator/experiment-runtime-identity.js.map +1 -0
  402. package/lib/orchestrator/experiment-selection.d.ts +25 -0
  403. package/lib/orchestrator/experiment-selection.d.ts.map +1 -0
  404. package/lib/orchestrator/experiment-selection.js +286 -0
  405. package/lib/orchestrator/experiment-selection.js.map +1 -0
  406. package/lib/orchestrator/experiment.d.ts +11 -0
  407. package/lib/orchestrator/experiment.d.ts.map +1 -0
  408. package/lib/orchestrator/experiment.js +639 -0
  409. package/lib/orchestrator/experiment.js.map +1 -0
  410. package/lib/orchestrator/fixture-identity.d.ts +7 -0
  411. package/lib/orchestrator/fixture-identity.d.ts.map +1 -0
  412. package/lib/orchestrator/fixture-identity.js +55 -0
  413. package/lib/orchestrator/fixture-identity.js.map +1 -0
  414. package/lib/orchestrator/fs-read.d.ts +19 -0
  415. package/lib/orchestrator/fs-read.d.ts.map +1 -0
  416. package/lib/orchestrator/fs-read.js +113 -0
  417. package/lib/orchestrator/fs-read.js.map +1 -0
  418. package/lib/orchestrator/grader-experiment.d.ts +190 -0
  419. package/lib/orchestrator/grader-experiment.d.ts.map +1 -0
  420. package/lib/orchestrator/grader-experiment.js +579 -0
  421. package/lib/orchestrator/grader-experiment.js.map +1 -0
  422. package/lib/orchestrator/grading.d.ts +31 -0
  423. package/lib/orchestrator/grading.d.ts.map +1 -0
  424. package/lib/orchestrator/grading.js +186 -0
  425. package/lib/orchestrator/grading.js.map +1 -0
  426. package/lib/orchestrator/index.d.ts +22 -0
  427. package/lib/orchestrator/index.d.ts.map +1 -0
  428. package/lib/orchestrator/index.js +30 -0
  429. package/lib/orchestrator/index.js.map +1 -0
  430. package/lib/orchestrator/lifecycle-hook-execution.d.ts +4 -0
  431. package/lib/orchestrator/lifecycle-hook-execution.d.ts.map +1 -0
  432. package/lib/orchestrator/lifecycle-hook-execution.js +67 -0
  433. package/lib/orchestrator/lifecycle-hook-execution.js.map +1 -0
  434. package/lib/orchestrator/list-runs.d.ts +33 -0
  435. package/lib/orchestrator/list-runs.d.ts.map +1 -0
  436. package/lib/orchestrator/list-runs.js +308 -0
  437. package/lib/orchestrator/list-runs.js.map +1 -0
  438. package/lib/orchestrator/loader.d.ts +3 -0
  439. package/lib/orchestrator/loader.d.ts.map +1 -0
  440. package/lib/orchestrator/loader.js +17 -0
  441. package/lib/orchestrator/loader.js.map +1 -0
  442. package/lib/orchestrator/optimization-lifecycle.d.ts +12 -0
  443. package/lib/orchestrator/optimization-lifecycle.d.ts.map +1 -0
  444. package/lib/orchestrator/optimization-lifecycle.js +92 -0
  445. package/lib/orchestrator/optimization-lifecycle.js.map +1 -0
  446. package/lib/orchestrator/orchestrator.d.ts +144 -0
  447. package/lib/orchestrator/orchestrator.d.ts.map +1 -0
  448. package/lib/orchestrator/orchestrator.js +632 -0
  449. package/lib/orchestrator/orchestrator.js.map +1 -0
  450. package/lib/orchestrator/owned-lock.d.ts +17 -0
  451. package/lib/orchestrator/owned-lock.d.ts.map +1 -0
  452. package/lib/orchestrator/owned-lock.js +149 -0
  453. package/lib/orchestrator/owned-lock.js.map +1 -0
  454. package/lib/orchestrator/package-version.d.ts +2 -0
  455. package/lib/orchestrator/package-version.d.ts.map +1 -0
  456. package/lib/orchestrator/package-version.js +4 -0
  457. package/lib/orchestrator/package-version.js.map +1 -0
  458. package/lib/orchestrator/provenance.d.ts +5 -0
  459. package/lib/orchestrator/provenance.d.ts.map +1 -0
  460. package/lib/orchestrator/provenance.js +137 -0
  461. package/lib/orchestrator/provenance.js.map +1 -0
  462. package/lib/orchestrator/regrade.d.ts +80 -0
  463. package/lib/orchestrator/regrade.d.ts.map +1 -0
  464. package/lib/orchestrator/regrade.js +433 -0
  465. package/lib/orchestrator/regrade.js.map +1 -0
  466. package/lib/orchestrator/rubric-loader.d.ts +7 -0
  467. package/lib/orchestrator/rubric-loader.d.ts.map +1 -0
  468. package/lib/orchestrator/rubric-loader.js +65 -0
  469. package/lib/orchestrator/rubric-loader.js.map +1 -0
  470. package/lib/orchestrator/run-dir.d.ts +40 -0
  471. package/lib/orchestrator/run-dir.d.ts.map +1 -0
  472. package/lib/orchestrator/run-dir.js +161 -0
  473. package/lib/orchestrator/run-dir.js.map +1 -0
  474. package/lib/orchestrator/run-execution.d.ts +30 -0
  475. package/lib/orchestrator/run-execution.d.ts.map +1 -0
  476. package/lib/orchestrator/run-execution.js +531 -0
  477. package/lib/orchestrator/run-execution.js.map +1 -0
  478. package/lib/orchestrator/run-lifecycle.d.ts +115 -0
  479. package/lib/orchestrator/run-lifecycle.d.ts.map +1 -0
  480. package/lib/orchestrator/run-lifecycle.js +136 -0
  481. package/lib/orchestrator/run-lifecycle.js.map +1 -0
  482. package/lib/orchestrator/suite-compare.d.ts +76 -0
  483. package/lib/orchestrator/suite-compare.d.ts.map +1 -0
  484. package/lib/orchestrator/suite-compare.js +159 -0
  485. package/lib/orchestrator/suite-compare.js.map +1 -0
  486. package/lib/orchestrator/suite-lifecycle.d.ts +157 -0
  487. package/lib/orchestrator/suite-lifecycle.d.ts.map +1 -0
  488. package/lib/orchestrator/suite-lifecycle.js +253 -0
  489. package/lib/orchestrator/suite-lifecycle.js.map +1 -0
  490. package/lib/orchestrator/suite-runs.d.ts +91 -0
  491. package/lib/orchestrator/suite-runs.d.ts.map +1 -0
  492. package/lib/orchestrator/suite-runs.js +157 -0
  493. package/lib/orchestrator/suite-runs.js.map +1 -0
  494. package/lib/orchestrator/suite-summary.d.ts +109 -0
  495. package/lib/orchestrator/suite-summary.d.ts.map +1 -0
  496. package/lib/orchestrator/suite-summary.js +213 -0
  497. package/lib/orchestrator/suite-summary.js.map +1 -0
  498. package/lib/orchestrator/trace-limits.d.ts +20 -0
  499. package/lib/orchestrator/trace-limits.d.ts.map +1 -0
  500. package/lib/orchestrator/trace-limits.js +28 -0
  501. package/lib/orchestrator/trace-limits.js.map +1 -0
  502. package/lib/orchestrator/validate-plugin-config.d.ts +10 -0
  503. package/lib/orchestrator/validate-plugin-config.d.ts.map +1 -0
  504. package/lib/orchestrator/validate-plugin-config.js +26 -0
  505. package/lib/orchestrator/validate-plugin-config.js.map +1 -0
  506. package/package.json +84 -0
  507. package/skills/lux/SKILL.md +85 -0
  508. package/skills/lux/references/cli.md +102 -0
  509. package/skills/lux/references/eval-authoring.md +105 -0
  510. package/skills/lux-answerer/SKILL.md +169 -0
  511. package/src/answerers/claude-code/index.ts +342 -0
  512. package/src/answerers/persona/index.ts +327 -0
  513. package/src/answerers/scripted/index.ts +186 -0
  514. package/src/answerers/shared.ts +23 -0
  515. package/src/answerers/terminal/index.ts +117 -0
  516. package/src/assertions/core/command-exits-zero.ts +94 -0
  517. package/src/assertions/core/event-checks.ts +138 -0
  518. package/src/assertions/core/file-checks.ts +336 -0
  519. package/src/assertions/core/helpers.ts +203 -0
  520. package/src/assertions/core/index.ts +80 -0
  521. package/src/assertions/core/json-pointer-equals.ts +84 -0
  522. package/src/assertions/core/output-checks.ts +134 -0
  523. package/src/assertions/core/predicate.ts +67 -0
  524. package/src/assertions/core/resource-checks.ts +180 -0
  525. package/src/assertions/core/safe-regex.ts +196 -0
  526. package/src/assertions/core/tool-called-with.ts +68 -0
  527. package/src/assertions/core/tool-calls.ts +28 -0
  528. package/src/assertions/core/tool-invocation.ts +198 -0
  529. package/src/assertions/core/tool-result-checks.ts +211 -0
  530. package/src/assertions/mcp/discovery-checks.ts +139 -0
  531. package/src/assertions/mcp/index.ts +37 -0
  532. package/src/assertions/rubric/index.ts +258 -0
  533. package/src/assertions/rubric/internal.ts +367 -0
  534. package/src/authoring.ts +2 -0
  535. package/src/cli/bin.ts +14 -0
  536. package/src/cli/command-runtime.ts +34 -0
  537. package/src/cli/init.ts +410 -0
  538. package/src/cli/program.ts +992 -0
  539. package/src/cli/render/format.ts +69 -0
  540. package/src/cli/render/reporter.ts +643 -0
  541. package/src/cli/render/tty.ts +314 -0
  542. package/src/cli/run-options.ts +82 -0
  543. package/src/cli/skills.ts +129 -0
  544. package/src/core/annotation.ts +104 -0
  545. package/src/core/answerer.ts +75 -0
  546. package/src/core/artifact-evidence.ts +249 -0
  547. package/src/core/artifact-snapshot.ts +429 -0
  548. package/src/core/assertion.ts +156 -0
  549. package/src/core/case.ts +103 -0
  550. package/src/core/conversation-turn.ts +47 -0
  551. package/src/core/driver.ts +75 -0
  552. package/src/core/emitter.ts +38 -0
  553. package/src/core/events.ts +135 -0
  554. package/src/core/exec.ts +263 -0
  555. package/src/core/experiment.ts +467 -0
  556. package/src/core/glob.ts +31 -0
  557. package/src/core/hash.ts +237 -0
  558. package/src/core/index.ts +28 -0
  559. package/src/core/lifecycle-fixtures.ts +145 -0
  560. package/src/core/lifecycle-hooks.ts +101 -0
  561. package/src/core/mcp-events.ts +57 -0
  562. package/src/core/model-cli.ts +271 -0
  563. package/src/core/observer.ts +50 -0
  564. package/src/core/platform-process.ts +284 -0
  565. package/src/core/registry.ts +57 -0
  566. package/src/core/rubric.ts +52 -0
  567. package/src/core/run.ts +144 -0
  568. package/src/core/statistics.ts +194 -0
  569. package/src/core/subject.ts +50 -0
  570. package/src/core/tool-events.ts +292 -0
  571. package/src/core/usage.ts +123 -0
  572. package/src/core/wire.ts +135 -0
  573. package/src/core/zod.ts +20 -0
  574. package/src/drivers/claude-code/index.ts +866 -0
  575. package/src/drivers/claude-code/parse-events.ts +464 -0
  576. package/src/drivers/codex/app-events.ts +218 -0
  577. package/src/drivers/codex/config.ts +128 -0
  578. package/src/drivers/codex/index.ts +995 -0
  579. package/src/drivers/mcp/index.ts +351 -0
  580. package/src/drivers/mcp/transport.ts +222 -0
  581. package/src/drivers/shared/artifacts.ts +86 -0
  582. package/src/drivers/shared/secure-copy.ts +40 -0
  583. package/src/drivers/subprocess/index.ts +490 -0
  584. package/src/index.ts +594 -0
  585. package/src/optimization/reflective.ts +319 -0
  586. package/src/orchestrator/annotation-loader.ts +97 -0
  587. package/src/orchestrator/annotation-store.ts +112 -0
  588. package/src/orchestrator/authored-dependencies.ts +193 -0
  589. package/src/orchestrator/campaign-lifecycle.ts +181 -0
  590. package/src/orchestrator/candidate-integrity.ts +18 -0
  591. package/src/orchestrator/candidates.ts +683 -0
  592. package/src/orchestrator/compare.ts +358 -0
  593. package/src/orchestrator/comparison-identity.ts +45 -0
  594. package/src/orchestrator/config.ts +205 -0
  595. package/src/orchestrator/content-bound-json.ts +71 -0
  596. package/src/orchestrator/discover.ts +132 -0
  597. package/src/orchestrator/doctor.ts +781 -0
  598. package/src/orchestrator/driver-session.ts +507 -0
  599. package/src/orchestrator/evaluator-identity.ts +45 -0
  600. package/src/orchestrator/experiment-budget.ts +61 -0
  601. package/src/orchestrator/experiment-context.ts +41 -0
  602. package/src/orchestrator/experiment-contracts.ts +43 -0
  603. package/src/orchestrator/experiment-corpus.ts +416 -0
  604. package/src/orchestrator/experiment-evaluation.ts +749 -0
  605. package/src/orchestrator/experiment-finish.ts +33 -0
  606. package/src/orchestrator/experiment-identity.ts +356 -0
  607. package/src/orchestrator/experiment-loader.ts +105 -0
  608. package/src/orchestrator/experiment-report.ts +788 -0
  609. package/src/orchestrator/experiment-runs.ts +960 -0
  610. package/src/orchestrator/experiment-runtime-identity.ts +465 -0
  611. package/src/orchestrator/experiment-selection.ts +467 -0
  612. package/src/orchestrator/experiment.ts +991 -0
  613. package/src/orchestrator/fixture-identity.ts +67 -0
  614. package/src/orchestrator/fs-read.ts +126 -0
  615. package/src/orchestrator/grader-experiment.ts +856 -0
  616. package/src/orchestrator/grading.ts +260 -0
  617. package/src/orchestrator/index.ts +186 -0
  618. package/src/orchestrator/lifecycle-hook-execution.ts +90 -0
  619. package/src/orchestrator/list-runs.ts +384 -0
  620. package/src/orchestrator/loader.ts +19 -0
  621. package/src/orchestrator/optimization-lifecycle.ts +126 -0
  622. package/src/orchestrator/orchestrator.ts +919 -0
  623. package/src/orchestrator/owned-lock.ts +147 -0
  624. package/src/orchestrator/package-version.ts +5 -0
  625. package/src/orchestrator/provenance.ts +140 -0
  626. package/src/orchestrator/regrade.ts +580 -0
  627. package/src/orchestrator/rubric-loader.ts +74 -0
  628. package/src/orchestrator/run-dir.ts +209 -0
  629. package/src/orchestrator/run-execution.ts +694 -0
  630. package/src/orchestrator/run-lifecycle.ts +174 -0
  631. package/src/orchestrator/suite-compare.ts +195 -0
  632. package/src/orchestrator/suite-lifecycle.ts +335 -0
  633. package/src/orchestrator/suite-runs.ts +202 -0
  634. package/src/orchestrator/suite-summary.ts +276 -0
  635. package/src/orchestrator/trace-limits.ts +65 -0
  636. package/src/orchestrator/validate-plugin-config.ts +34 -0
@@ -0,0 +1,992 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { lstat, mkdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
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";
8
+ import {
9
+ type Candidate,
10
+ GraderAnnotationSchema,
11
+ matchesAnyGlob,
12
+ valueHash,
13
+ } from "../core/index.js";
14
+ import {
15
+ applyCandidate,
16
+ buildRegistryFromConfig,
17
+ compareRuns,
18
+ compareSuiteRuns,
19
+ type DiscoveredCase,
20
+ discoverCases,
21
+ doctorProject,
22
+ experimentCampaignMode,
23
+ formatComparisonText,
24
+ formatDoctorReport,
25
+ formatExperimentDecisionMarkdown,
26
+ formatExperimentRun,
27
+ formatExperimentRunsTable,
28
+ formatRunDetail,
29
+ formatRunsTable,
30
+ formatSuiteComparisonText,
31
+ formatSuiteRunDetail,
32
+ formatSuiteRunsTable,
33
+ hasRegression,
34
+ hasSuiteRegression,
35
+ isExperimentRun,
36
+ listExperimentRuns,
37
+ listRuns,
38
+ listSuiteRuns,
39
+ loadEvalCase,
40
+ loadExperimentCampaign,
41
+ loadExperimentRun,
42
+ loadFrozenRegradableRun,
43
+ loadLuxConfig,
44
+ loadRubricDefinition,
45
+ loadRunDetail,
46
+ loadSuiteRun,
47
+ loadVerifiedExperimentDecisionSnapshot,
48
+ Orchestrator,
49
+ optimizeExperimentCampaign,
50
+ planExperimentCampaign,
51
+ regradeRun,
52
+ releaseFrozenRegradableRun,
53
+ resolveExperimentCampaignPath,
54
+ runExperimentCampaign,
55
+ type SuiteCaseResult,
56
+ type SuiteItem,
57
+ selectCases,
58
+ upsertGraderAnnotation,
59
+ verifyExperimentDecisionIntegrity,
60
+ verifyExperimentRunIntegrity,
61
+ } from "../orchestrator/index.js";
62
+ import {
63
+ abortOnInterrupt,
64
+ ensureDirectory,
65
+ failWith,
66
+ relativePathWithin,
67
+ } from "./command-runtime.js";
68
+ import { formatInitReport, initProject } from "./init.js";
69
+ import { ConsoleReporter } from "./render/reporter.js";
70
+ import {
71
+ collectTags,
72
+ collectValues,
73
+ parseAnnotationAssertion,
74
+ parsePositiveInt,
75
+ parseUnitInterval,
76
+ type RunOptions,
77
+ resolveAnswererOverride,
78
+ resolveConcurrency,
79
+ } from "./run-options.js";
80
+ import {
81
+ type BundledSkillName,
82
+ formatSkillsInstall,
83
+ formatSkillsList,
84
+ installSkills,
85
+ type SkillsPlatform,
86
+ } from "./skills.js";
87
+
88
+ const requireFromHere = createRequire(import.meta.url);
89
+ // From lib/cli/program.js (or src/cli/program.ts under test) to the package root.
90
+ const PKG_VERSION = (requireFromHere("../../package.json") as { version: string }).version;
91
+
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;
131
+ },
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 } : {}),
146
+ });
147
+ process.stdout.write(`${formatSkillsInstall(installed, cwd)}\n`);
148
+ },
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);
163
+ });
164
+
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);
193
+ });
194
+
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
+ );
210
+ };
211
+
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);
250
+ });
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
+ }
364
+ },
365
+ );
366
+ };
367
+
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
+ );
434
+ });
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);
616
+ });
617
+ };
618
+
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;
632
+ };
633
+
634
+ const caseIdOf = (casePath: string): string => basename(casePath, extname(casePath));
635
+
636
+ const SIGINT_EXIT_CODE = 130;
637
+
638
+ const physicalDestinationPath = async (path: string): Promise<string> => {
639
+ const suffix: string[] = [];
640
+ let ancestor = resolve(path);
641
+ while (!existsSync(ancestor)) {
642
+ const parent = dirname(ancestor);
643
+ if (parent === ancestor) break;
644
+ suffix.unshift(basename(ancestor));
645
+ ancestor = parent;
646
+ }
647
+ return resolve(await realpath(ancestor), ...suffix);
648
+ };
649
+
650
+ const assertReportDestination = (
651
+ out: string,
652
+ experimentDirectory: string,
653
+ subjectRoot: string,
654
+ excludedSubjectPaths: readonly string[],
655
+ ): void => {
656
+ if (relativePathWithin(experimentDirectory, out) !== null) {
657
+ throw new Error("lux report: --out must not write into the managed experiment directory");
658
+ }
659
+ const fromSubject = relativePathWithin(subjectRoot, out);
660
+ if (fromSubject !== null && !matchesAnyGlob(fromSubject, excludedSubjectPaths)) {
661
+ throw new Error(
662
+ "lux report: --out would change the experiment subject identity; " +
663
+ "write outside the subject or under an excluded runs/report directory",
664
+ );
665
+ }
666
+ };
667
+
668
+ const writeReportSafely = async (path: string, body: string, force: boolean): Promise<void> => {
669
+ if (!force) {
670
+ await writeFile(path, body, { flag: "wx" });
671
+ return;
672
+ }
673
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
674
+ try {
675
+ await writeFile(temporary, body, { flag: "wx" });
676
+ await rename(temporary, path);
677
+ } finally {
678
+ await rm(temporary, { force: true });
679
+ }
680
+ };
681
+
682
+ const resolveAuthoredFile = async (value: string, label: string): Promise<string> => {
683
+ if (value.startsWith("/")) {
684
+ if (existsSync(value) && (await stat(value)).isFile()) return resolve(value);
685
+ throw new Error(`lux: ${label} file does not exist: ${value}`);
686
+ }
687
+ let directory = process.cwd();
688
+ for (;;) {
689
+ const candidate = resolve(directory, value);
690
+ if (existsSync(candidate) && (await stat(candidate)).isFile()) return candidate;
691
+ const parent = dirname(directory);
692
+ if (parent === directory) break;
693
+ directory = parent;
694
+ }
695
+ throw new Error(`lux: ${label} file does not exist from ${process.cwd()}: ${value}`);
696
+ };
697
+
698
+ /**
699
+ * A case is only a success if it finished cleanly AND nothing graded it as
700
+ * failing. A run that timed out carrying no assertions grades as `null`, which
701
+ * must not be mistaken for a pass.
702
+ */
703
+ const isFailure = (outcome: { exitReason: string; casePassed: boolean | null }): boolean =>
704
+ outcome.exitReason !== "done" || outcome.casePassed !== true;
705
+
706
+ const firstLine = (message: string): string => (message.split("\n")[0] ?? message).trim();
707
+
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
+ const experimentCommand = async (
789
+ mode: "experiment" | "optimize",
790
+ campaignPath: string,
791
+ options: { runsRoot?: string; resume?: string; json?: boolean; plan?: boolean },
792
+ ): Promise<void> => {
793
+ const cwd = process.cwd();
794
+ const config = await loadLuxConfig(cwd);
795
+ const campaign = await resolveExperimentCampaignPath(config.rootDir, campaignPath);
796
+ const loaded = await loadExperimentCampaign(campaign);
797
+ experimentCampaignMode(campaign, loaded.campaign, mode);
798
+ const registry = buildRegistryFromConfig(config);
799
+ const controller = abortOnInterrupt();
800
+ const execution = {
801
+ config,
802
+ registry,
803
+ loaded,
804
+ ...(options.runsRoot ? { runsRoot: resolve(options.runsRoot) } : {}),
805
+ ...(options.resume ? { resumeFrom: resolve(options.resume) } : {}),
806
+ abortSignal: controller.signal,
807
+ };
808
+ if (options.plan) {
809
+ 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");
831
+ }
832
+ process.stdout.write(output);
833
+ if (!plan.fitsProjectedCallBudgets) failWith(1);
834
+ return;
835
+ }
836
+ const result =
837
+ mode === "optimize"
838
+ ? await optimizeExperimentCampaign(execution)
839
+ : 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
+ if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
846
+ else if (result.manifest.status !== "passed") failWith(1);
847
+ };
848
+
849
+ const runCommand = async (filters: string[], options: RunOptions): Promise<void> => {
850
+ if (options.interactive && options.claudeAnswerer) {
851
+ throw new Error("lux run: --interactive and --claude-answerer are mutually exclusive");
852
+ }
853
+ const cwd = process.cwd();
854
+ const config = await loadLuxConfig(cwd);
855
+ const profile = options.profile ? config.profiles?.[options.profile] : undefined;
856
+ if (options.profile && !profile) {
857
+ 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;
863
+ }
864
+ const harness = profile?.harness ?? config.harness;
865
+ const registry = buildRegistryFromConfig(config).withAssertion(createRubricAssertion(harness));
866
+ const defaultDriver = profile?.driver ?? config.defaultDriver;
867
+ const orchestrator = new Orchestrator({
868
+ registry,
869
+ defaultAnswerer: profile?.answerer ?? config.defaultAnswerer,
870
+ ...(defaultDriver ? { defaultDriver } : {}),
871
+ ...(harness ? { harness } : {}),
872
+ lifecycleFixtures: config.lifecycleFixtures,
873
+ });
874
+ const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
875
+ const subjectRoot = options.subjectRoot ? resolve(options.subjectRoot) : config.subjectRoot;
876
+ // A config file is the user's opt-in to tree-wide discovery. Without one,
877
+ // stay scoped to the `cases/` scaffold: discovery imports every .ts it
878
+ // visits, and running module-level side effects across an arbitrary cwd
879
+ // must never be the default.
880
+ const root = config.casesRoot ?? (config.configPath ? config.rootDir : join(cwd, "cases"));
881
+ // A configured casesRoot is a path the user declared; its absence is a
882
+ // broken config, not an empty suite — never mask it behind "no cases
883
+ // matched" or a lucky explicit-file run.
884
+ if (config.casesRoot && !existsSync(config.casesRoot)) {
885
+ process.stderr.write(`lux: casesRoot does not exist: ${config.casesRoot}\n`);
886
+ failWith(1);
887
+ return;
888
+ }
889
+
890
+ const { explicit, patterns } = partitionFilters(filters, cwd);
891
+ const tags = options.tag ?? [];
892
+ const hasFilter = explicit.length > 0 || patterns.length > 0 || tags.length > 0;
893
+
894
+ // Explicitly named files need no discovery, so they run even without a
895
+ // project; anything else would need a tree walk with nowhere safe to walk.
896
+ 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;
902
+ }
903
+
904
+ const { cases, problems } = existsSync(root)
905
+ ? await discoverCases(root)
906
+ : { cases: [], problems: [] };
907
+ for (const problem of problems) {
908
+ process.stderr.write(`lux: skipped ${problem.path}: ${firstLine(problem.reason)}\n`);
909
+ }
910
+
911
+ const selected = selectSuite(cases, { explicit, patterns }, tags);
912
+
913
+ // An empty selection fails every mode the same way — running, `--list`, and
914
+ // `--list-tags` all exit 1, so a typo'd filter can never read as a clean pass.
915
+ 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;
923
+ }
924
+
925
+ // List the tag vocabulary of the selected cases — "which --tag values exist?".
926
+ if (options.listTags) {
927
+ const counts = new Map<string, number>();
928
+ 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;
936
+ }
937
+
938
+ const base: SuiteItem[] = selected.map((c) => ({ path: c.path, id: c.id }));
939
+
940
+ // Collect-only: answer "what would run?" without spawning a single agent.
941
+ 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;
945
+ }
946
+
947
+ const loop = options.loop ?? 1;
948
+ const items = expandLoop(base, loop);
949
+
950
+ const { concurrency, note } = resolveConcurrency(options, config.concurrency);
951
+ if (note) process.stderr.write(note);
952
+
953
+ const reporter = new ConsoleReporter({ verbose: options.verbose ?? false });
954
+ const controller = abortOnInterrupt();
955
+ reporter.begin({
956
+ title: suiteTitle(patterns, tags, explicit, loop),
957
+ caseIds: items.map((i) => i.id),
958
+ runsRoot,
959
+ loop,
960
+ });
961
+
962
+ let results: SuiteCaseResult[] = [];
963
+ try {
964
+ const suite = await orchestrator.runSuite(items, {
965
+ runsRoot,
966
+ fixturesRoot: config.fixturesRoot,
967
+ answererOverride: resolveAnswererOverride(options),
968
+ observer: reporter,
969
+ abortSignal: controller.signal,
970
+ concurrency,
971
+ subjectRoot,
972
+ profileId: options.profile,
973
+ });
974
+ results = suite.results;
975
+ reporter.setSuiteDir(suite.suiteDir);
976
+ } finally {
977
+ reporter.finish();
978
+ }
979
+
980
+ if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
981
+ else if (results.some(isFailure)) failWith(1);
982
+ };
983
+
984
+ // Internal exports for testing
985
+ export {
986
+ expandLoop,
987
+ isFailure,
988
+ parsePositiveInt,
989
+ partitionFilters,
990
+ resolveConcurrency,
991
+ selectSuite,
992
+ };