@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,960 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import {
4
+ appendFile,
5
+ link,
6
+ lstat,
7
+ mkdir,
8
+ open,
9
+ readdir,
10
+ readFile,
11
+ realpath,
12
+ rename,
13
+ unlink,
14
+ writeFile,
15
+ } from "node:fs/promises";
16
+ import { hostname } from "node:os";
17
+ import { basename, join, relative, resolve, sep } from "node:path";
18
+ import { z } from "zod";
19
+ import {
20
+ type Candidate,
21
+ CandidateEvaluationSchema,
22
+ CandidateIdSchema,
23
+ CandidateSchema,
24
+ ConfidenceIntervalSchema,
25
+ type ExperimentCampaign,
26
+ ExperimentCampaignSchema,
27
+ emptyUsage,
28
+ TokenUsageSchema,
29
+ totalTokens,
30
+ } from "../core/index.js";
31
+ import { assertCandidateEnvelopeIntegrity, candidateEnvelopeHash } from "./candidate-integrity.js";
32
+ import { safeReadJson } from "./fs-read.js";
33
+
34
+ const EXPERIMENTS_DIR = ".experiments";
35
+ const MANIFEST_FILE = "experiment.json";
36
+ const EXECUTION_LOCK_FILE = ".execution.lock";
37
+ const ACCOUNTING_JOURNAL_FILE = "accounting.jsonl";
38
+
39
+ const ExperimentEvaluationSchema = CandidateEvaluationSchema.extend({
40
+ candidateId: CandidateIdSchema,
41
+ repetitions: z.number().int().positive(),
42
+ summaryPath: z.exactOptional(z.string()),
43
+ evidencePath: z.exactOptional(z.string()),
44
+ });
45
+ export type ExperimentEvaluation = z.infer<typeof ExperimentEvaluationSchema>;
46
+
47
+ const ExperimentCandidateRefSchema = z.object({
48
+ id: CandidateIdSchema,
49
+ path: z.string(),
50
+ candidateHash: z.string(),
51
+ sourceBytes: z.number().int().nonnegative(),
52
+ /** Hash of the complete schema-normalized Candidate record, not only its components. */
53
+ candidateEnvelopeHash: z.string().optional(),
54
+ });
55
+
56
+ const ExperimentBudgetUsageSchema = z.object({
57
+ metricCalls: z.number().int().nonnegative(),
58
+ judgeCalls: z.number().int().nonnegative(),
59
+ unpricedModelCalls: z.number().int().nonnegative().optional(),
60
+ proposalCalls: z.number().int().nonnegative(),
61
+ usage: TokenUsageSchema,
62
+ costUsd: z.number().nonnegative(),
63
+ });
64
+ export type ExperimentBudgetUsage = z.infer<typeof ExperimentBudgetUsageSchema>;
65
+
66
+ const AccountingDeltaSchema = ExperimentBudgetUsageSchema.partial();
67
+ type AccountingDelta = z.infer<typeof AccountingDeltaSchema>;
68
+ const AccountingJournalEntrySchema = z
69
+ .object({
70
+ id: z.string().min(1),
71
+ delta: AccountingDeltaSchema,
72
+ completedModelAttemptIds: z.array(z.string().min(1)).optional(),
73
+ })
74
+ .strict();
75
+
76
+ export const ExperimentSelectionSchema = z.object({
77
+ decidedAt: z.string(),
78
+ /** Candidate population used for the final ranking; excludes discarded search-only candidates. */
79
+ decisionCandidateIds: z.array(CandidateIdSchema).optional(),
80
+ /** Optimize-only checkpoint naming the final promotion population. */
81
+ promotionFinalistCandidateIds: z.array(CandidateIdSchema).optional(),
82
+ rankedCandidateIds: z.array(CandidateIdSchema),
83
+ eligibleCandidateIds: z.array(CandidateIdSchema),
84
+ championCandidateId: z.exactOptional(CandidateIdSchema),
85
+ constraints: z.array(
86
+ z.object({
87
+ candidateId: CandidateIdSchema,
88
+ metric: z.string(),
89
+ profile: z.exactOptional(z.string()),
90
+ split: z.enum(["train", "validation", "test"]),
91
+ values: z.array(z.number()),
92
+ passed: z.boolean(),
93
+ interval: z.exactOptional(ConfidenceIntervalSchema),
94
+ note: z.exactOptional(z.string()),
95
+ }),
96
+ ),
97
+ });
98
+ export type ExperimentSelection = z.infer<typeof ExperimentSelectionSchema>;
99
+
100
+ export const ExperimentRunSchema = z
101
+ .object({
102
+ schemaVersion: z.literal(1),
103
+ experimentId: z.string(),
104
+ campaignId: z.string(),
105
+ campaignPath: z.string(),
106
+ campaignHash: z.string(),
107
+ campaignSnapshot: ExperimentCampaignSchema,
108
+ resolvedProfiles: z.record(z.string(), z.unknown()),
109
+ evaluatorVersion: z.literal(1),
110
+ luxVersion: z.string(),
111
+ evaluationKind: z.enum(["agent", "grader"]).default("agent"),
112
+ evaluationHash: z.exactOptional(z.string()),
113
+ mode: z.enum(["experiment", "optimize"]),
114
+ status: z.enum(["running", "passed", "failed", "aborted"]),
115
+ startedAt: z.string(),
116
+ endedAt: z.string().nullable(),
117
+ /** Accumulated active execution time across completed process segments. */
118
+ activeTimeMs: z.number().nonnegative().default(0),
119
+ /** Start of the process segment that currently owns this running ledger. */
120
+ activeSegmentStartedAt: z.exactOptional(z.string()),
121
+ subjectRoot: z.string(),
122
+ baseSourceHash: z.string(),
123
+ /** Present after legacy candidate refs have been upgraded to full-envelope integrity. */
124
+ candidateIntegrityVersion: z.exactOptional(z.literal(1)),
125
+ candidates: z.array(ExperimentCandidateRefSchema),
126
+ evaluations: z.array(ExperimentEvaluationSchema),
127
+ /** Unit IDs whose observed model spend has been folded into budgetUsage. */
128
+ accountedEvaluationUnits: z.array(z.string()).default([]),
129
+ /**
130
+ * Number of durable accounting journal entries folded into budgetUsage.
131
+ * The append-only journal avoids rewriting an ever-growing manifest after
132
+ * every individual grader assertion.
133
+ */
134
+ accountingJournalEntries: z.number().int().nonnegative().default(0),
135
+ budgetUsage: ExperimentBudgetUsageSchema,
136
+ championCandidateId: z.exactOptional(CandidateIdSchema),
137
+ selection: z.exactOptional(ExperimentSelectionSchema),
138
+ errorMessage: z.exactOptional(z.string()),
139
+ })
140
+ .superRefine((manifest, ctx) => {
141
+ if (manifest.candidateIntegrityVersion !== 1) return;
142
+ for (const [index, candidate] of manifest.candidates.entries()) {
143
+ if (candidate.candidateEnvelopeHash !== undefined) continue;
144
+ ctx.addIssue({
145
+ code: "custom",
146
+ path: ["candidates", index, "candidateEnvelopeHash"],
147
+ message: "required for candidate integrity version 1",
148
+ });
149
+ }
150
+ });
151
+ export type ExperimentRun = z.infer<typeof ExperimentRunSchema>;
152
+
153
+ const timestampId = (date: Date): string => date.toISOString().replace(/[:.]/g, "-");
154
+
155
+ const writeJsonAtomic = async (path: string, value: unknown): Promise<void> => {
156
+ const temporary = `${path}.${process.pid}.tmp`;
157
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`);
158
+ await rename(temporary, path);
159
+ };
160
+
161
+ const persistJsonImmutable = async (path: string, value: unknown): Promise<void> => {
162
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
163
+ try {
164
+ const handle = await open(temporary, "wx");
165
+ try {
166
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`);
167
+ await handle.sync();
168
+ } finally {
169
+ await handle.close();
170
+ }
171
+ try {
172
+ // A hard link publishes the fully written inode without replacing an
173
+ // existing candidate. EEXIST is handled by comparing the persisted
174
+ // envelope in saveCandidate.
175
+ await link(temporary, path);
176
+ } catch (error) {
177
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
178
+ }
179
+ } finally {
180
+ await unlink(temporary).catch((error: NodeJS.ErrnoException) => {
181
+ if (error.code !== "ENOENT") throw error;
182
+ });
183
+ }
184
+ };
185
+
186
+ export type OpenedExperimentRun = {
187
+ path: string;
188
+ manifest: ExperimentRun;
189
+ write: () => Promise<void>;
190
+ recordAccounting: (id: string, delta: AccountingDelta) => Promise<boolean>;
191
+ /**
192
+ * Journal a provider attempt before it can spend. A start without a matching
193
+ * completion is charged as one unpriced call when the ledger resumes.
194
+ */
195
+ startModelAttempt: (kind: ModelAttemptKind) => Promise<string>;
196
+ /** Mark attempts complete only after their observed spend is journaled. */
197
+ completeModelAttempts: (attemptIds: readonly string[]) => Promise<void>;
198
+ /** Atomically journal observed spend and terminate its provider attempts. */
199
+ settleModelAttempts: (
200
+ id: string,
201
+ delta: AccountingDelta,
202
+ attemptIds: readonly string[],
203
+ ) => Promise<boolean>;
204
+ saveCandidate: (candidate: Candidate) => Promise<string>;
205
+ release: () => Promise<void>;
206
+ };
207
+
208
+ const ModelAttemptKindSchema = z.enum(["agent", "answerer", "judge", "proposal"]);
209
+ type ModelAttemptKind = z.infer<typeof ModelAttemptKindSchema>;
210
+ const MODEL_ATTEMPT_START_PREFIX = "model-attempt-start:";
211
+ const MODEL_ATTEMPT_COMPLETE_PREFIX = "model-attempt-complete:";
212
+ const MODEL_ATTEMPT_ABANDONED_PREFIX = "model-attempt-abandoned:";
213
+ const ExecutionLockSchema = z
214
+ .object({
215
+ pid: z.number().int().positive(),
216
+ hostname: z.string().min(1),
217
+ acquiredAt: z.string().datetime(),
218
+ })
219
+ .strict();
220
+
221
+ const modelAttemptParts = (
222
+ id: string,
223
+ prefix: string,
224
+ ): { kind: ModelAttemptKind; attemptId: string } | null => {
225
+ if (!id.startsWith(prefix)) return null;
226
+ const [kind, attemptId] = id.slice(prefix.length).split(":", 2);
227
+ const parsedKind = ModelAttemptKindSchema.safeParse(kind);
228
+ if (!parsedKind.success || !attemptId) return null;
229
+ return { kind: parsedKind.data, attemptId };
230
+ };
231
+
232
+ const modelAttemptId = (prefix: string, kind: ModelAttemptKind, attemptId: string): string =>
233
+ `${prefix}${kind}:${attemptId}`;
234
+
235
+ const acquireExecutionLock = async (path: string): Promise<{ release: () => Promise<void> }> => {
236
+ const lockPath = join(path, EXECUTION_LOCK_FILE);
237
+ const acquire = async (
238
+ allowStaleRecovery: boolean,
239
+ ): Promise<Awaited<ReturnType<typeof open>>> => {
240
+ try {
241
+ const handle = await open(lockPath, "wx");
242
+ const owner = ExecutionLockSchema.parse({
243
+ pid: process.pid,
244
+ hostname: hostname(),
245
+ acquiredAt: new Date().toISOString(),
246
+ });
247
+ await handle.writeFile(`${JSON.stringify(owner)}\n`);
248
+ return handle;
249
+ } catch (error) {
250
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
251
+ if (!allowStaleRecovery) {
252
+ throw new Error(`experiment is already owned by another process: ${path}`);
253
+ }
254
+ let rawOwner: unknown;
255
+ try {
256
+ rawOwner = JSON.parse(await readFile(lockPath, "utf8"));
257
+ } catch {
258
+ throw new Error(`experiment execution lock is present but unreadable: ${lockPath}`);
259
+ }
260
+ const parsedOwner = ExecutionLockSchema.safeParse(rawOwner);
261
+ if (!parsedOwner.success || parsedOwner.data.hostname !== hostname()) {
262
+ throw new Error(`experiment is already owned by another process: ${path}`);
263
+ }
264
+ const owner = parsedOwner.data;
265
+ try {
266
+ process.kill(owner.pid, 0);
267
+ throw new Error(`experiment is already owned by process ${owner.pid}: ${path}`);
268
+ } catch (ownerError) {
269
+ if ((ownerError as NodeJS.ErrnoException).code !== "ESRCH") throw ownerError;
270
+ }
271
+ await unlink(lockPath);
272
+ return acquire(false);
273
+ }
274
+ };
275
+ const handle = await acquire(true);
276
+ let released = false;
277
+ return {
278
+ release: async () => {
279
+ if (released) return;
280
+ released = true;
281
+ await handle.close();
282
+ await unlink(lockPath).catch((error: NodeJS.ErrnoException) => {
283
+ if (error.code !== "ENOENT") throw error;
284
+ });
285
+ },
286
+ };
287
+ };
288
+
289
+ const openedRun = (
290
+ path: string,
291
+ manifest: ExperimentRun,
292
+ release: () => Promise<void>,
293
+ accountedIds: Set<string>,
294
+ ): OpenedExperimentRun => {
295
+ const write = (): Promise<void> => {
296
+ if (manifest.status !== "running" && manifest.activeSegmentStartedAt) {
297
+ const segmentStartedAt = Date.parse(manifest.activeSegmentStartedAt);
298
+ const segmentEndedAt = manifest.endedAt ? Date.parse(manifest.endedAt) : Date.now();
299
+ if (Number.isFinite(segmentStartedAt) && Number.isFinite(segmentEndedAt)) {
300
+ manifest.activeTimeMs += Math.max(0, segmentEndedAt - segmentStartedAt);
301
+ }
302
+ delete manifest.activeSegmentStartedAt;
303
+ }
304
+ return writeJsonAtomic(join(path, MANIFEST_FILE), manifest);
305
+ };
306
+ const appendAccounting = async (
307
+ id: string,
308
+ rawDelta: AccountingDelta,
309
+ completedModelAttemptIds: readonly string[] = [],
310
+ ): Promise<boolean> => {
311
+ if (accountedIds.has(id)) return false;
312
+ const delta = AccountingDeltaSchema.parse(rawDelta);
313
+ const completed = [...new Set(completedModelAttemptIds)];
314
+ for (const attemptId of completed) {
315
+ if (!modelAttemptParts(attemptId, MODEL_ATTEMPT_START_PREFIX)) {
316
+ throw new Error(`invalid model attempt start id: ${attemptId}`);
317
+ }
318
+ }
319
+ const entry = AccountingJournalEntrySchema.parse({
320
+ id,
321
+ delta,
322
+ ...(completed.length > 0 ? { completedModelAttemptIds: completed } : {}),
323
+ });
324
+ await appendFile(join(path, ACCOUNTING_JOURNAL_FILE), `${JSON.stringify(entry)}\n`, "utf8");
325
+ accountedIds.add(id);
326
+ applyAccountingDelta(manifest.budgetUsage, delta);
327
+ manifest.accountingJournalEntries++;
328
+ return true;
329
+ };
330
+ const recordAccounting = (id: string, delta: AccountingDelta): Promise<boolean> =>
331
+ appendAccounting(id, delta);
332
+ const startModelAttempt = async (kind: ModelAttemptKind): Promise<string> => {
333
+ const attemptId = randomUUID();
334
+ await recordAccounting(
335
+ modelAttemptId(MODEL_ATTEMPT_START_PREFIX, kind, attemptId),
336
+ kind === "judge" ? { judgeCalls: 1 } : {},
337
+ );
338
+ return modelAttemptId(MODEL_ATTEMPT_START_PREFIX, kind, attemptId);
339
+ };
340
+ const completeModelAttempts = async (attemptIds: readonly string[]): Promise<void> => {
341
+ for (const startId of new Set(attemptIds)) {
342
+ const attempt = modelAttemptParts(startId, MODEL_ATTEMPT_START_PREFIX);
343
+ if (!attempt) throw new Error(`invalid model attempt start id: ${startId}`);
344
+ await recordAccounting(
345
+ modelAttemptId(MODEL_ATTEMPT_COMPLETE_PREFIX, attempt.kind, attempt.attemptId),
346
+ {},
347
+ );
348
+ }
349
+ };
350
+ const settleModelAttempts = (
351
+ id: string,
352
+ delta: AccountingDelta,
353
+ attemptIds: readonly string[],
354
+ ): Promise<boolean> => appendAccounting(id, delta, attemptIds);
355
+ const saveCandidate = async (candidate: Candidate): Promise<string> => {
356
+ if (manifest.candidateIntegrityVersion !== 1) {
357
+ throw new Error("legacy experiment candidate refs must be migrated before saving");
358
+ }
359
+ const verified = assertCandidateEnvelopeIntegrity(candidate);
360
+ const envelopeHash = candidateEnvelopeHash(verified);
361
+ const matches = manifest.candidates.filter((entry) => entry.id === verified.id);
362
+ if (matches.length > 1) {
363
+ throw new Error(`experiment candidate ID '${verified.id}' is duplicated`);
364
+ }
365
+ const existing = matches[0];
366
+ if (existing) {
367
+ const persisted = await verifiedCandidate(path, manifest, existing);
368
+ if (candidateEnvelopeHash(persisted) !== envelopeHash) {
369
+ throw new Error(
370
+ `experiment candidate '${verified.id}' already exists with a different envelope`,
371
+ );
372
+ }
373
+ // Upgrade a legacy v1 reference after proving its persisted candidate is
374
+ // equivalent across the complete schema-normalized envelope.
375
+ if (existing.candidateEnvelopeHash === undefined) {
376
+ existing.candidateEnvelopeHash = envelopeHash;
377
+ await write();
378
+ }
379
+ return containedPath(path, existing.path);
380
+ }
381
+
382
+ const candidatesDir = containedPath(path, "candidates");
383
+ const candidatesDirInfo = await lstat(candidatesDir);
384
+ if (candidatesDirInfo.isSymbolicLink() || !candidatesDirInfo.isDirectory()) {
385
+ throw new Error("experiment candidates directory must be a real directory");
386
+ }
387
+ if ((await realpath(candidatesDir)) !== resolve(await realpath(path), "candidates")) {
388
+ throw new Error("experiment candidates directory escapes its physical ledger");
389
+ }
390
+ const candidatePath = containedPath(path, `candidates/${verified.id}.json`);
391
+ const ref = {
392
+ id: verified.id,
393
+ path: relative(path, candidatePath),
394
+ candidateHash: verified.candidateHash,
395
+ sourceBytes: verified.sourceBytes,
396
+ candidateEnvelopeHash: envelopeHash,
397
+ };
398
+ await persistJsonImmutable(candidatePath, verified);
399
+ const persisted = await verifiedCandidate(path, manifest, ref);
400
+ if (candidateEnvelopeHash(persisted) !== envelopeHash) {
401
+ throw new Error(
402
+ `experiment candidate '${verified.id}' already exists with a different envelope`,
403
+ );
404
+ }
405
+ manifest.candidates.push(ref);
406
+ await write();
407
+ return candidatePath;
408
+ };
409
+ return {
410
+ path,
411
+ manifest,
412
+ write,
413
+ recordAccounting,
414
+ startModelAttempt,
415
+ completeModelAttempts,
416
+ settleModelAttempts,
417
+ saveCandidate,
418
+ release,
419
+ };
420
+ };
421
+
422
+ const applyAccountingDelta = (usage: ExperimentBudgetUsage, delta: AccountingDelta): void => {
423
+ usage.metricCalls += delta.metricCalls ?? 0;
424
+ usage.judgeCalls += delta.judgeCalls ?? 0;
425
+ usage.proposalCalls += delta.proposalCalls ?? 0;
426
+ usage.unpricedModelCalls = (usage.unpricedModelCalls ?? 0) + (delta.unpricedModelCalls ?? 0);
427
+ usage.usage = {
428
+ input: usage.usage.input + (delta.usage?.input ?? 0),
429
+ output: usage.usage.output + (delta.usage?.output ?? 0),
430
+ cacheRead: usage.usage.cacheRead + (delta.usage?.cacheRead ?? 0),
431
+ cacheCreation: usage.usage.cacheCreation + (delta.usage?.cacheCreation ?? 0),
432
+ };
433
+ usage.costUsd += delta.costUsd ?? 0;
434
+ };
435
+
436
+ const loadAccountingJournal = async (
437
+ path: string,
438
+ checkpointedEntries: number,
439
+ ): Promise<z.infer<typeof AccountingJournalEntrySchema>[]> => {
440
+ const journalPath = join(path, ACCOUNTING_JOURNAL_FILE);
441
+ if (!existsSync(journalPath)) return [];
442
+ const text = await readFile(journalPath, "utf8");
443
+ const lines = text.split("\n").filter((line) => line.trim().length > 0);
444
+ const entries: z.infer<typeof AccountingJournalEntrySchema>[] = [];
445
+ for (const [index, line] of lines.entries()) {
446
+ try {
447
+ let parsed: unknown;
448
+ try {
449
+ parsed = JSON.parse(line);
450
+ } catch {
451
+ throw new Error(`invalid accounting journal JSON at line ${index + 1}`);
452
+ }
453
+ const result = AccountingJournalEntrySchema.safeParse(parsed);
454
+ if (!result.success) {
455
+ throw new Error(`invalid accounting journal entry at line ${index + 1}`);
456
+ }
457
+ entries.push(result.data);
458
+ } catch (error) {
459
+ const isUncheckpointedTail = index >= checkpointedEntries && index === lines.length - 1;
460
+ if (!isUncheckpointedTail) throw error;
461
+ // appendFile can be torn by a hard kill. Only an invalid final entry
462
+ // beyond the manifest checkpoint is safe to discard.
463
+ await writeFile(
464
+ journalPath,
465
+ entries.length > 0 ? `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n` : "",
466
+ );
467
+ break;
468
+ }
469
+ }
470
+ return entries;
471
+ };
472
+
473
+ const containedPath = (rootPath: string, relativePath: string): string => {
474
+ const root = resolve(rootPath);
475
+ const path = resolve(root, relativePath);
476
+ if (path === root || !path.startsWith(`${root}${sep}`)) {
477
+ throw new Error(`experiment candidate path escapes its ledger: ${relativePath}`);
478
+ }
479
+ return path;
480
+ };
481
+
482
+ const verifiedCandidate = async (
483
+ experimentDir: string,
484
+ manifest: ExperimentRun,
485
+ ref: z.infer<typeof ExperimentCandidateRefSchema>,
486
+ ): Promise<Candidate> => {
487
+ const path = containedPath(experimentDir, ref.path);
488
+ const root = resolve(experimentDir);
489
+ const pathFromRoot = relative(root, path);
490
+ const components = pathFromRoot.split(sep);
491
+ let parent = root;
492
+ for (const component of components.slice(0, -1)) {
493
+ parent = join(parent, component);
494
+ const parentInfo = await lstat(parent).catch((error: NodeJS.ErrnoException) => {
495
+ if (error.code === "ENOENT") return null;
496
+ throw error;
497
+ });
498
+ if (!parentInfo || parentInfo.isSymbolicLink() || !parentInfo.isDirectory()) {
499
+ throw new Error(`experiment candidate '${ref.id}' has an invalid path component`);
500
+ }
501
+ }
502
+ const info = await lstat(path).catch((error: NodeJS.ErrnoException) => {
503
+ if (error.code === "ENOENT") return null;
504
+ throw error;
505
+ });
506
+ if (!info || info.isSymbolicLink() || !info.isFile()) {
507
+ throw new Error(`experiment candidate '${ref.id}' is missing or invalid`);
508
+ }
509
+ const physicalRoot = await realpath(root);
510
+ const physicalPath = await realpath(path);
511
+ if (physicalPath !== resolve(physicalRoot, pathFromRoot)) {
512
+ throw new Error(`experiment candidate '${ref.id}' escapes the physical ledger`);
513
+ }
514
+ const parsedCandidate = await safeReadJson(path, CandidateSchema);
515
+ if (!parsedCandidate) throw new Error(`experiment candidate '${ref.id}' is missing or invalid`);
516
+ let candidate: Candidate;
517
+ try {
518
+ candidate = assertCandidateEnvelopeIntegrity(parsedCandidate);
519
+ } catch {
520
+ throw new Error(`experiment candidate '${ref.id}' failed its integrity check`);
521
+ }
522
+ if (
523
+ candidate.id !== ref.id ||
524
+ candidate.candidateHash !== ref.candidateHash ||
525
+ candidate.sourceBytes !== ref.sourceBytes ||
526
+ candidate.baseSourceHash !== manifest.baseSourceHash ||
527
+ (ref.candidateEnvelopeHash !== undefined &&
528
+ candidateEnvelopeHash(candidate) !== ref.candidateEnvelopeHash)
529
+ ) {
530
+ throw new Error(`experiment candidate '${ref.id}' failed its integrity check`);
531
+ }
532
+ return candidate;
533
+ };
534
+
535
+ const migrateLegacyCandidateIntegrity = async (
536
+ experimentDir: string,
537
+ manifest: ExperimentRun,
538
+ ): Promise<void> => {
539
+ if (manifest.candidateIntegrityVersion === 1) return;
540
+ for (const ref of manifest.candidates) {
541
+ ref.candidateEnvelopeHash = candidateEnvelopeHash(
542
+ await verifiedCandidate(experimentDir, manifest, ref),
543
+ );
544
+ }
545
+ manifest.candidateIntegrityVersion = 1;
546
+ };
547
+
548
+ export const openExperimentRun = async (options: {
549
+ runsRoot: string;
550
+ campaignId: string;
551
+ campaignPath: string;
552
+ campaignHash: string;
553
+ campaignSnapshot: ExperimentCampaign;
554
+ resolvedProfiles: Record<string, unknown>;
555
+ luxVersion: string;
556
+ subjectRoot: string;
557
+ baseSourceHash: string;
558
+ mode: ExperimentRun["mode"];
559
+ evaluationKind?: ExperimentRun["evaluationKind"];
560
+ evaluationHash?: string;
561
+ startedAt?: Date;
562
+ }): Promise<OpenedExperimentRun> => {
563
+ const startedAt = options.startedAt ?? new Date();
564
+ const root = join(resolve(options.runsRoot), EXPERIMENTS_DIR);
565
+ await mkdir(root, { recursive: true });
566
+ const prefix = `${timestampId(startedAt)}-${options.campaignId.replace(/[^a-zA-Z0-9_-]+/g, "-")}`;
567
+ let experimentId = prefix;
568
+ let path = join(root, experimentId);
569
+ for (let suffix = 2; ; suffix++) {
570
+ try {
571
+ await mkdir(path);
572
+ break;
573
+ } catch (error) {
574
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
575
+ experimentId = `${prefix}-${suffix}`;
576
+ path = join(root, experimentId);
577
+ }
578
+ }
579
+ await mkdir(join(path, "candidates"), { recursive: true });
580
+ const manifest: ExperimentRun = {
581
+ schemaVersion: 1,
582
+ experimentId,
583
+ campaignId: options.campaignId,
584
+ campaignPath: options.campaignPath,
585
+ campaignHash: options.campaignHash,
586
+ campaignSnapshot: options.campaignSnapshot,
587
+ resolvedProfiles: options.resolvedProfiles,
588
+ evaluatorVersion: 1,
589
+ luxVersion: options.luxVersion,
590
+ evaluationKind: options.evaluationKind ?? "agent",
591
+ ...(options.evaluationHash ? { evaluationHash: options.evaluationHash } : {}),
592
+ mode: options.mode,
593
+ status: "running",
594
+ startedAt: startedAt.toISOString(),
595
+ endedAt: null,
596
+ activeTimeMs: 0,
597
+ activeSegmentStartedAt: startedAt.toISOString(),
598
+ subjectRoot: options.subjectRoot,
599
+ baseSourceHash: options.baseSourceHash,
600
+ candidateIntegrityVersion: 1,
601
+ candidates: [],
602
+ evaluations: [],
603
+ accountedEvaluationUnits: [],
604
+ accountingJournalEntries: 0,
605
+ budgetUsage: {
606
+ metricCalls: 0,
607
+ judgeCalls: 0,
608
+ unpricedModelCalls: 0,
609
+ proposalCalls: 0,
610
+ usage: emptyUsage(),
611
+ costUsd: 0,
612
+ },
613
+ };
614
+ const ownership = await acquireExecutionLock(path);
615
+ const opened = openedRun(path, manifest, ownership.release, new Set());
616
+ await opened.write();
617
+ return opened;
618
+ };
619
+
620
+ export const loadExperimentRun = async (path: string): Promise<ExperimentRun | null> =>
621
+ safeReadJson(join(path, MANIFEST_FILE), ExperimentRunSchema);
622
+
623
+ export const migrateExperimentCandidateIntegrity = async (path: string): Promise<ExperimentRun> => {
624
+ const absolute = resolve(path);
625
+ const ownership = await acquireExecutionLock(absolute);
626
+ try {
627
+ const manifest = await loadExperimentRun(absolute);
628
+ if (!manifest) throw new Error(`experiment manifest not found or invalid at ${absolute}`);
629
+ if (manifest.candidateIntegrityVersion !== 1) {
630
+ await migrateLegacyCandidateIntegrity(absolute, manifest);
631
+ await writeJsonAtomic(join(absolute, MANIFEST_FILE), manifest);
632
+ }
633
+ return manifest;
634
+ } finally {
635
+ await ownership.release();
636
+ }
637
+ };
638
+
639
+ export const experimentActiveElapsedMs = (
640
+ manifest: ExperimentRun,
641
+ nowMs: number = Date.now(),
642
+ ): number => {
643
+ const segmentStartedAt = manifest.activeSegmentStartedAt
644
+ ? Date.parse(manifest.activeSegmentStartedAt)
645
+ : Number.NaN;
646
+ return (
647
+ manifest.activeTimeMs +
648
+ (Number.isFinite(segmentStartedAt) ? Math.max(0, nowMs - segmentStartedAt) : 0)
649
+ );
650
+ };
651
+
652
+ export const resumeExperimentRun = async (path: string): Promise<OpenedExperimentRun> => {
653
+ const absolute = resolve(path);
654
+ const manifest = await loadExperimentRun(absolute);
655
+ if (!manifest) throw new Error(`experiment manifest not found or invalid at ${absolute}`);
656
+ if (manifest.status === "passed") {
657
+ throw new Error("passed experiments are immutable and cannot be resumed");
658
+ }
659
+ const ownership = await acquireExecutionLock(absolute);
660
+ try {
661
+ await migrateLegacyCandidateIntegrity(absolute, manifest);
662
+ const resumedAt = new Date();
663
+ if (manifest.activeSegmentStartedAt) {
664
+ const previousSegmentStart = Date.parse(manifest.activeSegmentStartedAt);
665
+ const previousSegmentEnd = manifest.endedAt
666
+ ? Date.parse(manifest.endedAt)
667
+ : resumedAt.getTime();
668
+ if (Number.isFinite(previousSegmentStart) && Number.isFinite(previousSegmentEnd)) {
669
+ // A running segment without a terminal checkpoint represents a hard
670
+ // kill. Charge the unknowable gap conservatively through recovery.
671
+ manifest.activeTimeMs += Math.max(0, previousSegmentEnd - previousSegmentStart);
672
+ }
673
+ } else if (manifest.activeTimeMs === 0) {
674
+ // Forward-compatible accounting for v1 ledgers written before active
675
+ // segments existed.
676
+ const legacyStart = Date.parse(manifest.startedAt);
677
+ const legacyEnd = manifest.endedAt ? Date.parse(manifest.endedAt) : resumedAt.getTime();
678
+ if (Number.isFinite(legacyStart) && Number.isFinite(legacyEnd)) {
679
+ manifest.activeTimeMs = Math.max(0, legacyEnd - legacyStart);
680
+ }
681
+ }
682
+ const journal = await loadAccountingJournal(absolute, manifest.accountingJournalEntries);
683
+ if (manifest.accountingJournalEntries > journal.length) {
684
+ throw new Error(
685
+ `accounting journal is truncated: manifest checkpoints ${manifest.accountingJournalEntries} entries but only ${journal.length} remain`,
686
+ );
687
+ }
688
+ const accountedIds = new Set(manifest.accountedEvaluationUnits);
689
+ for (const [index, entry] of journal.entries()) {
690
+ if (accountedIds.has(entry.id)) {
691
+ if (index >= manifest.accountingJournalEntries) {
692
+ throw new Error(`duplicate accounting journal id: ${entry.id}`);
693
+ }
694
+ continue;
695
+ }
696
+ accountedIds.add(entry.id);
697
+ if (index >= manifest.accountingJournalEntries) {
698
+ applyAccountingDelta(manifest.budgetUsage, entry.delta);
699
+ }
700
+ }
701
+ manifest.accountingJournalEntries = journal.length;
702
+ manifest.status = "running";
703
+ manifest.endedAt = null;
704
+ manifest.activeSegmentStartedAt = resumedAt.toISOString();
705
+ delete manifest.errorMessage;
706
+ const opened = openedRun(absolute, manifest, ownership.release, accountedIds);
707
+ const terminals = new Set(
708
+ journal.flatMap((entry) => {
709
+ const completed = modelAttemptParts(entry.id, MODEL_ATTEMPT_COMPLETE_PREFIX);
710
+ const abandoned = modelAttemptParts(entry.id, MODEL_ATTEMPT_ABANDONED_PREFIX);
711
+ return [
712
+ ...(completed ? [`${completed.kind}:${completed.attemptId}`] : []),
713
+ ...(abandoned ? [`${abandoned.kind}:${abandoned.attemptId}`] : []),
714
+ ...(entry.completedModelAttemptIds ?? []).flatMap((attemptId) => {
715
+ const settled = modelAttemptParts(attemptId, MODEL_ATTEMPT_START_PREFIX);
716
+ return settled ? [`${settled.kind}:${settled.attemptId}`] : [];
717
+ }),
718
+ ];
719
+ }),
720
+ );
721
+ for (const entry of journal) {
722
+ const started = modelAttemptParts(entry.id, MODEL_ATTEMPT_START_PREFIX);
723
+ if (!started || terminals.has(`${started.kind}:${started.attemptId}`)) continue;
724
+ // A hard-killed provider cannot report tokens or price. Charge one
725
+ // explicit unpriced attempt exactly once before any unit can rerun.
726
+ await opened.recordAccounting(
727
+ modelAttemptId(MODEL_ATTEMPT_ABANDONED_PREFIX, started.kind, started.attemptId),
728
+ { unpricedModelCalls: 1 },
729
+ );
730
+ }
731
+ await opened.write();
732
+ return opened;
733
+ } catch (error) {
734
+ await ownership.release();
735
+ throw error;
736
+ }
737
+ };
738
+
739
+ export const listExperimentRuns = async (
740
+ runsRoot: string,
741
+ ): Promise<Array<ExperimentRun & { experimentDir: string }>> => {
742
+ const root = join(resolve(runsRoot), EXPERIMENTS_DIR);
743
+ if (!existsSync(root)) return [];
744
+ const directories = (await readdir(root, { withFileTypes: true }))
745
+ .filter((entry) => entry.isDirectory())
746
+ .map((entry) => join(root, entry.name));
747
+ const runs = await Promise.all(
748
+ directories.map(async (experimentDir) => ({
749
+ experimentDir,
750
+ experiment: await loadExperimentRun(experimentDir),
751
+ })),
752
+ );
753
+ return runs
754
+ .flatMap(({ experimentDir, experiment }) =>
755
+ experiment ? [{ ...experiment, experimentDir }] : [],
756
+ )
757
+ .sort((a, b) => b.experimentId.localeCompare(a.experimentId));
758
+ };
759
+
760
+ export const loadExperimentCandidate = async (
761
+ experimentDir: string,
762
+ candidateId: string,
763
+ ): Promise<Candidate | null> => {
764
+ const experiment = await loadExperimentRun(experimentDir);
765
+ const ref = experiment?.candidates.find((entry) => entry.id === candidateId);
766
+ return experiment && ref ? verifiedCandidate(experimentDir, experiment, ref) : null;
767
+ };
768
+
769
+ export const loadExperimentCandidates = async (experimentDir: string): Promise<Candidate[]> => {
770
+ const experiment = await loadExperimentRun(experimentDir);
771
+ if (!experiment) throw new Error(`experiment manifest not found or invalid at ${experimentDir}`);
772
+ return Promise.all(
773
+ experiment.candidates.map((candidate) =>
774
+ verifiedCandidate(experimentDir, experiment, candidate),
775
+ ),
776
+ );
777
+ };
778
+
779
+ const strictAccountingJournal = async (
780
+ experimentDir: string,
781
+ ): Promise<z.infer<typeof AccountingJournalEntrySchema>[]> => {
782
+ const path = join(resolve(experimentDir), ACCOUNTING_JOURNAL_FILE);
783
+ const info = await lstat(path).catch((error: NodeJS.ErrnoException) => {
784
+ if (error.code === "ENOENT") return null;
785
+ throw error;
786
+ });
787
+ if (!info) return [];
788
+ if (info.isSymbolicLink() || !info.isFile()) {
789
+ throw new Error("experiment accounting journal must be a regular file");
790
+ }
791
+ return (await readFile(path, "utf8"))
792
+ .split("\n")
793
+ .filter((line) => line.trim().length > 0)
794
+ .map((line, index) => {
795
+ try {
796
+ return AccountingJournalEntrySchema.parse(JSON.parse(line));
797
+ } catch {
798
+ throw new Error(`invalid accounting journal entry at line ${index + 1}`);
799
+ }
800
+ });
801
+ };
802
+
803
+ export const verifyExperimentRunIntegrity = async (
804
+ experimentDir: string,
805
+ experiment: ExperimentRun,
806
+ ): Promise<Candidate[]> => {
807
+ const manifestPath = join(resolve(experimentDir), MANIFEST_FILE);
808
+ const manifestInfo = await lstat(manifestPath);
809
+ if (manifestInfo.isSymbolicLink() || !manifestInfo.isFile()) {
810
+ throw new Error("experiment manifest must be a regular file");
811
+ }
812
+ const candidateIds = experiment.candidates.map((candidate) => candidate.id);
813
+ const candidatePaths = experiment.candidates.map((candidate) => candidate.path);
814
+ if (new Set(candidateIds).size !== candidateIds.length) {
815
+ throw new Error("experiment candidate IDs must be unique");
816
+ }
817
+ if (new Set(candidatePaths).size !== candidatePaths.length) {
818
+ throw new Error("experiment candidate paths must be unique");
819
+ }
820
+ const candidates = await Promise.all(
821
+ experiment.candidates.map((candidate) =>
822
+ verifiedCandidate(experimentDir, experiment, candidate),
823
+ ),
824
+ );
825
+ const journal = await strictAccountingJournal(experimentDir);
826
+ if (journal.length !== experiment.accountingJournalEntries) {
827
+ throw new Error(
828
+ `accounting journal checkpoint mismatch: manifest=${experiment.accountingJournalEntries} journal=${journal.length}`,
829
+ );
830
+ }
831
+ const ids = new Set<string>();
832
+ const reconstructed: ExperimentBudgetUsage = {
833
+ metricCalls: 0,
834
+ judgeCalls: 0,
835
+ proposalCalls: 0,
836
+ unpricedModelCalls: 0,
837
+ usage: emptyUsage(),
838
+ costUsd: 0,
839
+ };
840
+ for (const entry of journal) {
841
+ if (ids.has(entry.id)) throw new Error(`duplicate accounting journal id: ${entry.id}`);
842
+ ids.add(entry.id);
843
+ applyAccountingDelta(reconstructed, entry.delta);
844
+ }
845
+ const actual = experiment.budgetUsage;
846
+ if (
847
+ reconstructed.metricCalls !== actual.metricCalls ||
848
+ reconstructed.judgeCalls !== actual.judgeCalls ||
849
+ reconstructed.proposalCalls !== actual.proposalCalls ||
850
+ (reconstructed.unpricedModelCalls ?? 0) !== (actual.unpricedModelCalls ?? 0) ||
851
+ reconstructed.usage.input !== actual.usage.input ||
852
+ reconstructed.usage.output !== actual.usage.output ||
853
+ reconstructed.usage.cacheRead !== actual.usage.cacheRead ||
854
+ reconstructed.usage.cacheCreation !== actual.usage.cacheCreation ||
855
+ reconstructed.costUsd !== actual.costUsd
856
+ ) {
857
+ throw new Error("experiment budget usage does not match its durable accounting journal");
858
+ }
859
+ return candidates;
860
+ };
861
+
862
+ export const isExperimentRun = (path: string): boolean => existsSync(join(path, MANIFEST_FILE));
863
+
864
+ const conciseNumber = (value: number): string =>
865
+ Number.isInteger(value) ? String(value) : value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
866
+
867
+ const conciseText = (value: string, maximum = 160): string =>
868
+ value.length <= maximum ? value : `${value.slice(0, maximum - 1)}…`;
869
+
870
+ export const formatExperimentRun = (path: string, experiment: ExperimentRun): string => {
871
+ const usage = experiment.budgetUsage.usage;
872
+ const tokens = totalTokens(usage);
873
+ const lines = [
874
+ `experimentDir: ${path}`,
875
+ `experiment: ${experiment.experimentId}`,
876
+ `campaign: ${experiment.campaignId}`,
877
+ `evaluation: ${experiment.evaluationKind}`,
878
+ `mode: ${experiment.mode}`,
879
+ `status: ${experiment.status}`,
880
+ `started: ${experiment.startedAt}`,
881
+ `ended: ${experiment.endedAt ?? "—"}`,
882
+ `candidates: ${experiment.candidates.length}`,
883
+ `evaluations: ${experiment.evaluations.length}`,
884
+ `champion: ${experiment.championCandidateId ?? "—"}`,
885
+ `spend: metric=${experiment.budgetUsage.metricCalls} judge=${experiment.budgetUsage.judgeCalls} proposal=${experiment.budgetUsage.proposalCalls} unpriced=${experiment.budgetUsage.unpricedModelCalls ?? 0}`,
886
+ `tokens: ${tokens} (input=${usage.input} output=${usage.output} cache-read=${usage.cacheRead} cache-create=${usage.cacheCreation})`,
887
+ `reported cost: $${experiment.budgetUsage.costUsd.toFixed(4)}`,
888
+ ];
889
+ if (experiment.errorMessage) lines.push(`error: ${experiment.errorMessage}`);
890
+ if (experiment.selection) {
891
+ lines.push(
892
+ `ranking: ${experiment.selection.rankedCandidateIds.join(" > ") || "—"}`,
893
+ `eligible: ${experiment.selection.eligibleCandidateIds.join(", ") || "none"}`,
894
+ "gates:",
895
+ );
896
+ for (const constraint of experiment.selection.constraints) {
897
+ const scope = [
898
+ constraint.profile ? `profile=${constraint.profile}` : null,
899
+ `split=${constraint.split}`,
900
+ ]
901
+ .filter(Boolean)
902
+ .join(" ");
903
+ const interval = constraint.interval
904
+ ? ` ${constraint.interval.method}=${conciseNumber(constraint.interval.estimate)} [${conciseNumber(constraint.interval.lower)},${conciseNumber(constraint.interval.upper)}] n=${constraint.interval.n}`
905
+ : "";
906
+ const note = constraint.note ? ` note=${conciseText(constraint.note)}` : "";
907
+ lines.push(
908
+ ` ${constraint.passed ? "✓" : "✗"} ${constraint.candidateId} ${constraint.metric} ${scope} values=${constraint.values.map(conciseNumber).join(",") || "—"}${interval}${note}`,
909
+ );
910
+ }
911
+ }
912
+ if (experiment.evaluations.length > 0) lines.push("evidence:");
913
+ for (const evaluation of experiment.evaluations) {
914
+ const evaluationTokens = evaluation.usage ? totalTokens(evaluation.usage) : 0;
915
+ lines.push(
916
+ ` ${evaluation.candidateId} profile=${evaluation.profileId} split=${evaluation.split} repetitions=${evaluation.repetitions} pass=${conciseNumber(evaluation.passRate)} calls=${evaluation.metricCalls ?? "—"}/${evaluation.judgeCalls ?? "—"} tokens=${evaluationTokens} unpriced=${evaluation.unpricedModelCalls ?? 0} cost=$${(evaluation.costUsd ?? 0).toFixed(4)}`,
917
+ ` cases: ${
918
+ Object.entries(evaluation.casePassRates)
919
+ .sort(([a], [b]) => a.localeCompare(b))
920
+ .map(([id, passRate]) => `${id}=${conciseNumber(passRate)}`)
921
+ .join(", ") || "—"
922
+ }`,
923
+ );
924
+ for (const failure of evaluation.failedCases) {
925
+ lines.push(
926
+ ` failure: ${failure.caseId} score=${failure.score === null ? "—" : conciseNumber(failure.score)} ${conciseText(failure.feedback[0] ?? "no feedback")}`,
927
+ );
928
+ }
929
+ }
930
+ if (experiment.candidates.length > 0) lines.push("candidates:");
931
+ for (const candidate of experiment.candidates) {
932
+ lines.push(
933
+ ` ${candidate.id === experiment.championCandidateId ? "★" : " "} ${candidate.id} ${candidate.sourceBytes} bytes ${basename(candidate.path)}`,
934
+ );
935
+ }
936
+ return lines.join("\n");
937
+ };
938
+
939
+ export const formatExperimentRunsTable = (
940
+ experiments: Array<ExperimentRun & { experimentDir: string }>,
941
+ ): string => {
942
+ if (experiments.length === 0) return "(no experiment runs found)";
943
+ const rows = [
944
+ ["status", "experiment", "campaign", "mode", "candidates", "champion"],
945
+ ...experiments.map((experiment) => [
946
+ experiment.status.toUpperCase(),
947
+ basename(experiment.experimentDir),
948
+ experiment.campaignId,
949
+ experiment.mode,
950
+ String(experiment.candidates.length),
951
+ experiment.championCandidateId ?? "—",
952
+ ]),
953
+ ];
954
+ const widths = (rows[0] ?? []).map((_, column) =>
955
+ Math.max(...rows.map((row) => row[column]?.length ?? 0)),
956
+ );
957
+ return rows
958
+ .map((row) => row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" "))
959
+ .join("\n");
960
+ };