@prismatic-io/lux 0.0.2-preview.18 → 0.0.2-preview.21

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 (244) hide show
  1. package/lib/answerers/persona/index.d.ts +5 -1
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/assertions/rubric/index.d.ts +3 -1
  4. package/lib/assertions/rubric/index.d.ts.map +1 -1
  5. package/lib/authoring.d.ts +3 -1
  6. package/lib/authoring.d.ts.map +1 -1
  7. package/lib/authoring.js +2 -1
  8. package/lib/authoring.js.map +1 -1
  9. package/lib/cli/harness-plan.d.ts +26 -0
  10. package/lib/cli/harness-plan.d.ts.map +1 -0
  11. package/lib/cli/harness-plan.js +75 -0
  12. package/lib/cli/harness-plan.js.map +1 -0
  13. package/lib/cli/init-templates.d.ts +2 -2
  14. package/lib/cli/init-templates.d.ts.map +1 -1
  15. package/lib/cli/init-templates.js +11 -4
  16. package/lib/cli/init-templates.js.map +1 -1
  17. package/lib/cli/init.d.ts +2 -0
  18. package/lib/cli/init.d.ts.map +1 -1
  19. package/lib/cli/init.js +6 -3
  20. package/lib/cli/init.js.map +1 -1
  21. package/lib/cli/output-schemas.d.ts +7 -0
  22. package/lib/cli/output-schemas.d.ts.map +1 -1
  23. package/lib/cli/program.d.ts +5 -10
  24. package/lib/cli/program.d.ts.map +1 -1
  25. package/lib/cli/program.js +44 -77
  26. package/lib/cli/program.js.map +1 -1
  27. package/lib/cli/run-options.d.ts +19 -0
  28. package/lib/cli/run-options.d.ts.map +1 -1
  29. package/lib/cli/run-options.js +92 -0
  30. package/lib/cli/run-options.js.map +1 -1
  31. package/lib/core/case.d.ts +2 -0
  32. package/lib/core/case.d.ts.map +1 -1
  33. package/lib/core/case.js +3 -0
  34. package/lib/core/case.js.map +1 -1
  35. package/lib/core/driver.d.ts +13 -0
  36. package/lib/core/driver.d.ts.map +1 -1
  37. package/lib/core/driver.js +10 -0
  38. package/lib/core/driver.js.map +1 -1
  39. package/lib/core/harness-catalog.d.ts +35 -0
  40. package/lib/core/harness-catalog.d.ts.map +1 -0
  41. package/lib/core/harness-catalog.js +45 -0
  42. package/lib/core/harness-catalog.js.map +1 -0
  43. package/lib/core/harness-catalogs/antigravity.d.ts +11 -0
  44. package/lib/core/harness-catalogs/antigravity.d.ts.map +1 -0
  45. package/lib/core/harness-catalogs/antigravity.js +23 -0
  46. package/lib/core/harness-catalogs/antigravity.js.map +1 -0
  47. package/lib/core/harness-catalogs/claude-code.d.ts +12 -0
  48. package/lib/core/harness-catalogs/claude-code.d.ts.map +1 -0
  49. package/lib/core/harness-catalogs/claude-code.js +24 -0
  50. package/lib/core/harness-catalogs/claude-code.js.map +1 -0
  51. package/lib/core/harness-catalogs/codex.d.ts +11 -0
  52. package/lib/core/harness-catalogs/codex.d.ts.map +1 -0
  53. package/lib/core/harness-catalogs/codex.js +26 -0
  54. package/lib/core/harness-catalogs/codex.js.map +1 -0
  55. package/lib/core/harness-catalogs/copilot.d.ts +29 -0
  56. package/lib/core/harness-catalogs/copilot.d.ts.map +1 -0
  57. package/lib/core/harness-catalogs/copilot.js +45 -0
  58. package/lib/core/harness-catalogs/copilot.js.map +1 -0
  59. package/lib/core/harness-catalogs/cursor.d.ts +2 -0
  60. package/lib/core/harness-catalogs/cursor.d.ts.map +1 -0
  61. package/lib/core/harness-catalogs/cursor.js +13 -0
  62. package/lib/core/harness-catalogs/cursor.js.map +1 -0
  63. package/lib/core/harness-catalogs/index.d.ts +67 -0
  64. package/lib/core/harness-catalogs/index.d.ts.map +1 -0
  65. package/lib/core/harness-catalogs/index.js +15 -0
  66. package/lib/core/harness-catalogs/index.js.map +1 -0
  67. package/lib/core/harness-matrix.d.ts +70 -0
  68. package/lib/core/harness-matrix.d.ts.map +1 -0
  69. package/lib/core/harness-matrix.js +194 -0
  70. package/lib/core/harness-matrix.js.map +1 -0
  71. package/lib/core/index.d.ts +3 -0
  72. package/lib/core/index.d.ts.map +1 -1
  73. package/lib/core/index.js +3 -0
  74. package/lib/core/index.js.map +1 -1
  75. package/lib/core/model-cli.d.ts +4 -11
  76. package/lib/core/model-cli.d.ts.map +1 -1
  77. package/lib/core/model-cli.js +23 -17
  78. package/lib/core/model-cli.js.map +1 -1
  79. package/lib/core/platform-process.d.ts +1 -0
  80. package/lib/core/platform-process.d.ts.map +1 -1
  81. package/lib/core/platform-process.js +1 -1
  82. package/lib/core/platform-process.js.map +1 -1
  83. package/lib/core/run.d.ts +4 -0
  84. package/lib/core/run.d.ts.map +1 -1
  85. package/lib/core/run.js +2 -0
  86. package/lib/core/run.js.map +1 -1
  87. package/lib/drivers/antigravity/config.d.ts +46 -0
  88. package/lib/drivers/antigravity/config.d.ts.map +1 -0
  89. package/lib/drivers/antigravity/config.js +39 -0
  90. package/lib/drivers/antigravity/config.js.map +1 -0
  91. package/lib/drivers/antigravity/events.d.ts +4 -0
  92. package/lib/drivers/antigravity/events.d.ts.map +1 -0
  93. package/lib/drivers/antigravity/events.js +193 -0
  94. package/lib/drivers/antigravity/events.js.map +1 -0
  95. package/lib/drivers/antigravity/index.d.ts +60 -0
  96. package/lib/drivers/antigravity/index.d.ts.map +1 -0
  97. package/lib/drivers/antigravity/index.js +90 -0
  98. package/lib/drivers/antigravity/index.js.map +1 -0
  99. package/lib/drivers/claude-code/index.d.ts +93 -7
  100. package/lib/drivers/claude-code/index.d.ts.map +1 -1
  101. package/lib/drivers/claude-code/index.js +14 -8
  102. package/lib/drivers/claude-code/index.js.map +1 -1
  103. package/lib/drivers/codex/config.d.ts +61 -4
  104. package/lib/drivers/codex/config.d.ts.map +1 -1
  105. package/lib/drivers/codex/config.js +19 -5
  106. package/lib/drivers/codex/config.js.map +1 -1
  107. package/lib/drivers/codex/index.d.ts +63 -5
  108. package/lib/drivers/codex/index.d.ts.map +1 -1
  109. package/lib/drivers/codex/index.js +11 -3
  110. package/lib/drivers/codex/index.js.map +1 -1
  111. package/lib/drivers/copilot/config.d.ts +81 -0
  112. package/lib/drivers/copilot/config.d.ts.map +1 -0
  113. package/lib/drivers/copilot/config.js +54 -0
  114. package/lib/drivers/copilot/config.js.map +1 -0
  115. package/lib/drivers/copilot/index.d.ts +103 -0
  116. package/lib/drivers/copilot/index.d.ts.map +1 -0
  117. package/lib/drivers/copilot/index.js +256 -0
  118. package/lib/drivers/copilot/index.js.map +1 -0
  119. package/lib/drivers/cursor/config.d.ts +6 -1
  120. package/lib/drivers/cursor/config.d.ts.map +1 -1
  121. package/lib/drivers/cursor/config.js +2 -3
  122. package/lib/drivers/cursor/config.js.map +1 -1
  123. package/lib/drivers/cursor/index.d.ts +7 -2
  124. package/lib/drivers/cursor/index.d.ts.map +1 -1
  125. package/lib/drivers/cursor/index.js +9 -6
  126. package/lib/drivers/cursor/index.js.map +1 -1
  127. package/lib/drivers/cursor/interaction.d.ts +1 -2
  128. package/lib/drivers/cursor/interaction.d.ts.map +1 -1
  129. package/lib/drivers/cursor/interaction.js +3 -14
  130. package/lib/drivers/cursor/interaction.js.map +1 -1
  131. package/lib/drivers/{cursor/events.d.ts → shared/acp-events.d.ts} +5 -3
  132. package/lib/drivers/shared/acp-events.d.ts.map +1 -0
  133. package/lib/drivers/{cursor/events.js → shared/acp-events.js} +20 -10
  134. package/lib/drivers/shared/acp-events.js.map +1 -0
  135. package/lib/drivers/shared/acp-interaction.d.ts +7 -0
  136. package/lib/drivers/shared/acp-interaction.d.ts.map +1 -0
  137. package/lib/drivers/shared/acp-interaction.js +20 -0
  138. package/lib/drivers/shared/acp-interaction.js.map +1 -0
  139. package/lib/drivers/{cursor → shared}/acp-transport.d.ts +2 -0
  140. package/lib/drivers/shared/acp-transport.d.ts.map +1 -0
  141. package/lib/drivers/{cursor → shared}/acp-transport.js +10 -10
  142. package/lib/drivers/shared/acp-transport.js.map +1 -0
  143. package/lib/index.d.ts +7 -2
  144. package/lib/index.d.ts.map +1 -1
  145. package/lib/index.js +7 -2
  146. package/lib/index.js.map +1 -1
  147. package/lib/optimization/reflective.d.ts +2 -0
  148. package/lib/optimization/reflective.d.ts.map +1 -1
  149. package/lib/orchestrator/comparison-identity.d.ts.map +1 -1
  150. package/lib/orchestrator/comparison-identity.js +1 -0
  151. package/lib/orchestrator/comparison-identity.js.map +1 -1
  152. package/lib/orchestrator/config.d.ts +7 -0
  153. package/lib/orchestrator/config.d.ts.map +1 -1
  154. package/lib/orchestrator/config.js +8 -1
  155. package/lib/orchestrator/config.js.map +1 -1
  156. package/lib/orchestrator/doctor.d.ts.map +1 -1
  157. package/lib/orchestrator/doctor.js +72 -2
  158. package/lib/orchestrator/doctor.js.map +1 -1
  159. package/lib/orchestrator/experiment-context.d.ts +1 -1
  160. package/lib/orchestrator/harness-catalog-drift.d.ts +22 -0
  161. package/lib/orchestrator/harness-catalog-drift.d.ts.map +1 -0
  162. package/lib/orchestrator/harness-catalog-drift.js +90 -0
  163. package/lib/orchestrator/harness-catalog-drift.js.map +1 -0
  164. package/lib/orchestrator/orchestrator.d.ts +9 -1
  165. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  166. package/lib/orchestrator/orchestrator.js +14 -22
  167. package/lib/orchestrator/orchestrator.js.map +1 -1
  168. package/lib/orchestrator/run-dir.d.ts +2 -0
  169. package/lib/orchestrator/run-dir.d.ts.map +1 -1
  170. package/lib/orchestrator/run-dir.js +1 -1
  171. package/lib/orchestrator/run-dir.js.map +1 -1
  172. package/lib/orchestrator/run-execution.d.ts +1 -0
  173. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  174. package/lib/orchestrator/run-execution.js +2 -0
  175. package/lib/orchestrator/run-execution.js.map +1 -1
  176. package/lib/orchestrator/suite-compare.d.ts +3 -0
  177. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  178. package/lib/orchestrator/suite-compare.js +27 -17
  179. package/lib/orchestrator/suite-compare.js.map +1 -1
  180. package/lib/orchestrator/suite-runs.d.ts +3 -0
  181. package/lib/orchestrator/suite-runs.d.ts.map +1 -1
  182. package/lib/orchestrator/suite-runs.js +2 -0
  183. package/lib/orchestrator/suite-runs.js.map +1 -1
  184. package/lib/orchestrator/suite-summary.d.ts +9 -0
  185. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  186. package/lib/orchestrator/suite-summary.js +14 -4
  187. package/lib/orchestrator/suite-summary.js.map +1 -1
  188. package/package.json +1 -1
  189. package/skills/lux-answerer/SKILL.md +1 -1
  190. package/src/authoring.ts +10 -3
  191. package/src/cli/harness-plan.ts +117 -0
  192. package/src/cli/init-templates.ts +12 -5
  193. package/src/cli/init.ts +12 -5
  194. package/src/cli/program.ts +50 -92
  195. package/src/cli/run-options.ts +123 -0
  196. package/src/core/case.ts +3 -0
  197. package/src/core/driver.ts +26 -0
  198. package/src/core/harness-catalog.ts +100 -0
  199. package/src/core/harness-catalogs/antigravity.ts +24 -0
  200. package/src/core/harness-catalogs/claude-code.ts +25 -0
  201. package/src/core/harness-catalogs/codex.ts +27 -0
  202. package/src/core/harness-catalogs/copilot.ts +46 -0
  203. package/src/core/harness-catalogs/cursor.ts +13 -0
  204. package/src/core/harness-catalogs/index.ts +22 -0
  205. package/src/core/harness-matrix.ts +298 -0
  206. package/src/core/index.ts +3 -0
  207. package/src/core/model-cli.ts +38 -21
  208. package/src/core/platform-process.ts +4 -4
  209. package/src/core/run.ts +2 -0
  210. package/src/drivers/antigravity/README.md +46 -0
  211. package/src/drivers/antigravity/config.ts +44 -0
  212. package/src/drivers/antigravity/events.ts +235 -0
  213. package/src/drivers/antigravity/index.ts +109 -0
  214. package/src/drivers/claude-code/index.ts +18 -7
  215. package/src/drivers/codex/config.ts +23 -5
  216. package/src/drivers/codex/index.ts +15 -2
  217. package/src/drivers/copilot/README.md +52 -0
  218. package/src/drivers/copilot/config.ts +58 -0
  219. package/src/drivers/copilot/index.ts +292 -0
  220. package/src/drivers/cursor/README.md +9 -6
  221. package/src/drivers/cursor/config.ts +2 -3
  222. package/src/drivers/cursor/index.ts +10 -6
  223. package/src/drivers/cursor/interaction.ts +3 -19
  224. package/src/drivers/{cursor/events.ts → shared/acp-events.ts} +20 -9
  225. package/src/drivers/shared/acp-interaction.ts +27 -0
  226. package/src/drivers/{cursor → shared}/acp-transport.ts +15 -10
  227. package/src/index.ts +44 -3
  228. package/src/orchestrator/comparison-identity.ts +1 -0
  229. package/src/orchestrator/config.ts +8 -0
  230. package/src/orchestrator/doctor.ts +125 -3
  231. package/src/orchestrator/harness-catalog-drift.ts +132 -0
  232. package/src/orchestrator/orchestrator.ts +31 -26
  233. package/src/orchestrator/run-dir.ts +8 -1
  234. package/src/orchestrator/run-execution.ts +3 -0
  235. package/src/orchestrator/suite-compare.ts +29 -19
  236. package/src/orchestrator/suite-runs.ts +3 -1
  237. package/src/orchestrator/suite-summary.ts +19 -4
  238. package/lib/drivers/cursor/acp-transport.d.ts.map +0 -1
  239. package/lib/drivers/cursor/acp-transport.js.map +0 -1
  240. package/lib/drivers/cursor/events.d.ts.map +0 -1
  241. package/lib/drivers/cursor/events.js.map +0 -1
  242. package/skills/lux/SKILL.md +0 -89
  243. package/skills/lux/references/cli.md +0 -128
  244. package/skills/lux/references/eval-authoring.md +0 -266
package/src/index.ts CHANGED
@@ -258,9 +258,11 @@ export {
258
258
  type DriverRuntime,
259
259
  defineDriver,
260
260
  driverRef,
261
+ type HarnessSelection,
261
262
  type ResolvedFixtures,
262
263
  ResolvedFixturesSchema,
263
264
  type StartContext,
265
+ selectModelAndReasoningEffort,
264
266
  } from "./core/driver.js";
265
267
  export {
266
268
  type DriverMetric,
@@ -344,6 +346,38 @@ export {
344
346
  type OptimizerInput,
345
347
  } from "./core/experiment.js";
346
348
  export { type GlobalIdParts, tryParseGlobalId } from "./core/global-id.js";
349
+ export {
350
+ type CatalogModels,
351
+ catalogEfforts,
352
+ defaultCatalogEffort,
353
+ defineHarnessCatalog,
354
+ HARNESS_EFFORTS,
355
+ type HarnessCatalog,
356
+ type ReasoningEffort,
357
+ ReasoningEffortSchema,
358
+ } from "./core/harness-catalog.js";
359
+ export {
360
+ antigravityCatalog,
361
+ type BuiltInHarnessDriver,
362
+ builtInHarnessCatalogs,
363
+ claudeCodeCatalog,
364
+ codexCatalog,
365
+ copilotCatalog,
366
+ cursorCatalog,
367
+ harnessCatalogFor,
368
+ } from "./core/harness-catalogs/index.js";
369
+ export {
370
+ type BuiltInHarnessMatrixEntry,
371
+ BuiltInHarnessMatrixEntrySchema,
372
+ type CustomHarnessMatrixEntry,
373
+ expandHarnessMatrix,
374
+ type HarnessMatrix,
375
+ type HarnessMatrixEntry,
376
+ HarnessMatrixEntrySchema,
377
+ HarnessMatrixSchema,
378
+ type HarnessPermutation,
379
+ permutationId,
380
+ } from "./core/harness-matrix.js";
347
381
  export {
348
382
  defineFixtures,
349
383
  type LifecycleFixture,
@@ -379,14 +413,11 @@ export {
379
413
  export {
380
414
  DEFAULT_CLAUDE_MODEL,
381
415
  DEFAULT_CODEX_MODEL,
382
- DEFAULT_CODEX_REASONING_EFFORT,
383
416
  defaultHarnessModel,
384
417
  type HarnessModelConfig,
385
418
  HarnessModelConfigSchema,
386
419
  type ModelCliProvider,
387
420
  ModelCliProviderSchema,
388
- type ReasoningEffort,
389
- ReasoningEffortSchema,
390
421
  } from "./core/model-cli.js";
391
422
  export {
392
423
  type AssertionProgress,
@@ -472,6 +503,11 @@ export {
472
503
  type WireEvent,
473
504
  WireEventSchema,
474
505
  } from "./core/wire.js";
506
+ export {
507
+ type AntigravityDriverConfig,
508
+ AntigravityDriverConfigSchema,
509
+ antigravityDriver,
510
+ } from "./drivers/antigravity/index.js";
475
511
  export {
476
512
  type ClaudeCodeDriverConfig,
477
513
  ClaudeCodeDriverConfigSchema,
@@ -482,6 +518,11 @@ export {
482
518
  CodexDriverConfigSchema,
483
519
  codexDriver,
484
520
  } from "./drivers/codex/index.js";
521
+ export {
522
+ type CopilotDriverConfig,
523
+ CopilotDriverConfigSchema,
524
+ copilotDriver,
525
+ } from "./drivers/copilot/index.js";
485
526
  export {
486
527
  type CursorDriverConfig,
487
528
  CursorDriverConfigSchema,
@@ -36,6 +36,7 @@ export const runComparisonIdentity = (metadata: RunMetadata): string =>
36
36
  assertionsHash: metadata.provenance?.assertionsHash,
37
37
  fixturesHash: metadata.provenance?.fixturesHash,
38
38
  profileId: metadata.provenance?.profileId,
39
+ permutationId: metadata.provenance?.permutationId,
39
40
  driverConfigHash: metadata.provenance?.driverConfigHash,
40
41
  answererConfigHash: metadata.provenance?.answererConfigHash,
41
42
  evaluatorConfigHash: metadata.provenance?.evaluatorConfigHash,
@@ -18,13 +18,16 @@ import {
18
18
  type DriverPlugin,
19
19
  DriverRefSchema,
20
20
  formatZodIssues,
21
+ HarnessMatrixSchema,
21
22
  HarnessModelConfigSchema,
22
23
  type LifecycleHooksPlugin,
23
24
  lifecycleHooksPluginIssue,
24
25
  Registry,
25
26
  } from "../core/index.js";
27
+ import { antigravityDriver } from "../drivers/antigravity/index.js";
26
28
  import { claudeCodeDriver } from "../drivers/claude-code/index.js";
27
29
  import { codexDriver } from "../drivers/codex/index.js";
30
+ import { copilotDriver } from "../drivers/copilot/index.js";
28
31
  import { cursorDriver } from "../drivers/cursor/index.js";
29
32
  import { mcpProbeDriver } from "../drivers/mcp/index.js";
30
33
  import { subprocessDriver } from "../drivers/subprocess/index.js";
@@ -80,6 +83,8 @@ const LuxConfigSchema = z
80
83
  subjectRoot: z.string().optional(),
81
84
  defaultAnswerer: AnswererRefSchema.default("persona"),
82
85
  defaultDriver: DriverRefSchema.optional(),
86
+ /** Driver, model, and effort permutations every case runs through by default. */
87
+ matrix: HarnessMatrixSchema.optional(),
83
88
  /** Shared provider/model defaults for persona answering and rubric judging. */
84
89
  harness: HarnessModelConfigSchema.optional(),
85
90
  concurrency: z.number().int().min(1).default(1),
@@ -166,6 +171,7 @@ export const loadLuxConfig = async (cwd: string = process.cwd()): Promise<Resolv
166
171
  ...(validated.fixturesRoot ? { fixturesRoot: resolve(root, validated.fixturesRoot) } : {}),
167
172
  defaultAnswerer: validated.defaultAnswerer,
168
173
  ...(validated.defaultDriver ? { defaultDriver: validated.defaultDriver } : {}),
174
+ ...(validated.matrix ? { matrix: validated.matrix } : {}),
169
175
  ...(validated.harness ? { harness: validated.harness } : {}),
170
176
  concurrency: validated.concurrency,
171
177
  ...(validated.environment ? { environment: validated.environment } : {}),
@@ -181,6 +187,8 @@ const builtinDrivers: DriverPlugin<never>[] = [
181
187
  claudeCodeDriver,
182
188
  codexDriver,
183
189
  cursorDriver,
190
+ copilotDriver,
191
+ antigravityDriver,
184
192
  subprocessDriver,
185
193
  mcpProbeDriver,
186
194
  ];
@@ -4,7 +4,15 @@ import { resolve } from "node:path";
4
4
  import { glob } from "tinyglobby";
5
5
  import { z } from "zod";
6
6
  import { RubricConfigSchema } from "../assertions/rubric/index.js";
7
- import type { EvalCase, Registry } from "../core/index.js";
7
+ import {
8
+ casePermutations,
9
+ type EvalCase,
10
+ expandHarnessMatrix,
11
+ type HarnessPermutation,
12
+ permutationDriverRef,
13
+ permutationId,
14
+ type Registry,
15
+ } from "../core/index.js";
8
16
  import { registryForEvalCase } from "./case-extensions.js";
9
17
  import { buildRegistryFromConfig, loadLuxConfig, type ResolvedLuxConfig } from "./config.js";
10
18
  import { type DiscoveredCase, type DiscoveryResult, discoverCases } from "./discover.js";
@@ -17,6 +25,12 @@ import {
17
25
  } from "./experiment-loader.js";
18
26
  import { cliIdentity } from "./experiment-runtime-identity.js";
19
27
  import { assertionConfigForSchema } from "./grading.js";
28
+ import {
29
+ describeHarnessCatalogDrift,
30
+ harnessCatalogDrift,
31
+ harnessCatalogProbes,
32
+ hasHarnessCatalogDrift,
33
+ } from "./harness-catalog-drift.js";
20
34
  import { loadEvalCase } from "./loader.js";
21
35
  import { validatePluginConfig, validatePluginConfigAsync } from "./validate-plugin-config.js";
22
36
 
@@ -132,7 +146,28 @@ const effectiveDriverReference = (local: unknown, fallback: unknown): unknown =>
132
146
  };
133
147
  };
134
148
 
135
- const caseDriverReferences = (local: unknown, config: ResolvedLuxConfig): unknown[] => {
149
+ const boundPermutation = (
150
+ permutation: HarnessPermutation,
151
+ config: ResolvedLuxConfig,
152
+ registry: Registry,
153
+ ): unknown => {
154
+ const plugin = registry.getDriver(permutation.driver);
155
+ if (!plugin) throw new Error(`unknown driver '${permutation.driver}'`);
156
+ return permutationDriverRef(permutation, config.defaultDriver, plugin);
157
+ };
158
+
159
+ const caseDriverReferences = (
160
+ authored: Pick<EvalCase, "driver" | "matrix">,
161
+ config: ResolvedLuxConfig,
162
+ registry: Registry,
163
+ ): unknown[] => {
164
+ const local = authored.driver;
165
+ const permutations = casePermutations(authored, config.matrix);
166
+ if (permutations.length > 0) {
167
+ return permutations.map((permutation) =>
168
+ effectiveDriverReference(local, boundPermutation(permutation, config, registry)),
169
+ );
170
+ }
136
171
  const fallbacks = [
137
172
  config.defaultDriver,
138
173
  ...Object.values(config.profiles ?? {}).map((profile) => profile.driver),
@@ -192,6 +227,17 @@ const configuredCliTargets = (config: ResolvedLuxConfig, registry: Registry): Cl
192
227
  };
193
228
  for (const command of driverCommands(config.defaultDriver, registry))
194
229
  add("default driver", command);
230
+ for (const permutation of expandHarnessMatrix(config.matrix ?? [])) {
231
+ try {
232
+ for (const command of driverCommands(
233
+ boundPermutation(permutation, config, registry),
234
+ registry,
235
+ ))
236
+ add(`matrix '${permutationId(permutation)}' driver`, command);
237
+ } catch {
238
+ /* Reported by validateConfiguredMatrix. */
239
+ }
240
+ }
195
241
  add("default answerer", answererCommand(config.defaultAnswerer));
196
242
  if (config.harness) {
197
243
  add(
@@ -357,6 +403,32 @@ const validateConfiguredDrivers = (
357
403
  }
358
404
  };
359
405
 
406
+ const validateConfiguredMatrix = (
407
+ config: ResolvedLuxConfig,
408
+ registry: Registry,
409
+ recorder: DoctorRecorder,
410
+ ): void => {
411
+ for (const permutation of expandHarnessMatrix(config.matrix ?? [])) {
412
+ const id = permutationId(permutation);
413
+ try {
414
+ const reference = boundPermutation(permutation, config, registry);
415
+ const plugin = registry.getDriver(permutation.driver);
416
+ validatePluginConfig(
417
+ plugin?.configSchema,
418
+ rawRefConfig(reference),
419
+ `matrix '${id}' driver '${permutation.driver}'`,
420
+ );
421
+ } catch (error) {
422
+ recorder.add(
423
+ `matrix:${id}`,
424
+ "fail",
425
+ `matrix permutation '${id}' is not runnable`,
426
+ firstLine(error),
427
+ );
428
+ }
429
+ }
430
+ };
431
+
360
432
  const validateConfiguredAnswerers = (
361
433
  config: ResolvedLuxConfig,
362
434
  registry: Registry,
@@ -576,7 +648,7 @@ const inspectCase = async (
576
648
  try {
577
649
  const authored = await loadEvalCase(discovered.path);
578
650
  const caseRegistry = registryForEvalCase(registry, authored);
579
- const drivers = caseDriverReferences(authored.driver, config);
651
+ const drivers = caseDriverReferences(authored, config, caseRegistry);
580
652
  const answerer = authored.answerer ?? config.defaultAnswerer;
581
653
  validateCaseDrivers(discovered.id, drivers, caseRegistry, recorder, authored.requires);
582
654
  validateCaseAnswerer(discovered.id, answerer, caseRegistry, recorder);
@@ -659,6 +731,54 @@ const inspectCliTargets = async (
659
731
  }
660
732
  };
661
733
 
734
+ const catalogDriverReferences = (config: ResolvedLuxConfig): unknown[] => [
735
+ ...configuredDriverReferences(config).map(([, reference]) => reference),
736
+ ...(config.matrix ?? []).map((entry) => ({ name: entry.driver, config: entry.config })),
737
+ ];
738
+
739
+ const configuredDriverCommand = (config: ResolvedLuxConfig, driver: string): string | undefined => {
740
+ for (const reference of catalogDriverReferences(config)) {
741
+ if (refName(reference) !== driver) continue;
742
+ const command = (rawRefConfig(reference) as { command?: unknown } | undefined)?.command;
743
+ if (typeof command === "string") return command;
744
+ }
745
+ return undefined;
746
+ };
747
+
748
+ const inspectHarnessCatalogs = async (
749
+ config: ResolvedLuxConfig,
750
+ recorder: DoctorRecorder,
751
+ ): Promise<void> => {
752
+ const configured = new Set(
753
+ catalogDriverReferences(config)
754
+ .map(refName)
755
+ .filter((name): name is string => name !== undefined),
756
+ );
757
+ for (const probe of harnessCatalogProbes) {
758
+ const driver = probe.catalog.driver;
759
+ if (!configured.has(driver)) continue;
760
+ const live = await probe.read(configuredDriverCommand(config, driver));
761
+ if (!live) {
762
+ recorder.add(
763
+ `catalog:${driver}`,
764
+ "warn",
765
+ `${driver} harness catalog (verified ${probe.catalog.verified}) could not be compared with the installed CLI`,
766
+ `${probe.source} was unavailable`,
767
+ );
768
+ continue;
769
+ }
770
+ const drift = harnessCatalogDrift(probe.catalog, live);
771
+ recorder.add(
772
+ `catalog:${driver}`,
773
+ hasHarnessCatalogDrift(drift) ? "warn" : "pass",
774
+ hasHarnessCatalogDrift(drift)
775
+ ? `${driver} harness catalog (verified ${probe.catalog.verified}) differs from the installed CLI`
776
+ : `${driver} harness catalog matches the installed CLI`,
777
+ describeHarnessCatalogDrift(drift) || undefined,
778
+ );
779
+ }
780
+ };
781
+
662
782
  const isBlockingCampaignWarning = (warning: string): boolean =>
663
783
  warning.includes("below confidence minSamples=") || warning.includes("cannot attain confidence");
664
784
 
@@ -752,6 +872,7 @@ export const doctorProject = async (options: DoctorOptions): Promise<DoctorRepor
752
872
  if (!registry) return recorder.finish(config.rootDir);
753
873
 
754
874
  validateConfiguredDrivers(config, registry, recorder);
875
+ validateConfiguredMatrix(config, registry, recorder);
755
876
  validateConfiguredAnswerers(config, registry, recorder);
756
877
  const caseCliTargets = await inspectCases(config, registry, recorder);
757
878
  if (parsedOptions.static) {
@@ -766,6 +887,7 @@ export const doctorProject = async (options: DoctorOptions): Promise<DoctorRepor
766
887
  config,
767
888
  recorder,
768
889
  );
890
+ await inspectHarnessCatalogs(config, recorder);
769
891
  }
770
892
  await inspectCampaigns(
771
893
  parsedOptions.campaigns ?? [],
@@ -0,0 +1,132 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import {
6
+ type ClosedHarnessCatalog,
7
+ codexCatalog,
8
+ copilotCatalog,
9
+ exec,
10
+ type ReasoningEffort,
11
+ tryParseJson,
12
+ } from "../core/index.js";
13
+
14
+ export type LiveHarnessModel = {
15
+ id: string;
16
+ efforts?: readonly ReasoningEffort[] | undefined;
17
+ };
18
+
19
+ export type HarnessCatalogDrift = {
20
+ unlisted: string[];
21
+ mismatched: string[];
22
+ unavailable: string[];
23
+ };
24
+
25
+ export const harnessCatalogDrift = (
26
+ catalog: ClosedHarnessCatalog,
27
+ live: readonly LiveHarnessModel[],
28
+ ): HarnessCatalogDrift => {
29
+ const liveIds = new Set(live.map((model) => model.id));
30
+ return {
31
+ unlisted: live.filter((model) => !(model.id in catalog.models)).map((model) => model.id),
32
+ mismatched: live
33
+ .filter(
34
+ (model) =>
35
+ model.efforts !== undefined &&
36
+ model.id in catalog.models &&
37
+ model.efforts.join(",") !== catalog.models[model.id]?.join(","),
38
+ )
39
+ .map((model) => `${model.id} (${model.efforts?.join(", ") || "no effort"})`),
40
+ unavailable: Object.keys(catalog.models).filter((id) => !liveIds.has(id)),
41
+ };
42
+ };
43
+
44
+ export const hasHarnessCatalogDrift = (drift: HarnessCatalogDrift): boolean =>
45
+ drift.unlisted.length > 0 || drift.mismatched.length > 0;
46
+
47
+ export const describeHarnessCatalogDrift = (drift: HarnessCatalogDrift): string =>
48
+ [
49
+ drift.unlisted.length > 0 ? `not in catalog: ${drift.unlisted.join(", ")}` : undefined,
50
+ drift.mismatched.length > 0 ? `efforts differ: ${drift.mismatched.join("; ")}` : undefined,
51
+ drift.unavailable.length > 0 ? `not offered here: ${drift.unavailable.join(", ")}` : undefined,
52
+ ]
53
+ .filter((part) => part !== undefined)
54
+ .join(". ");
55
+
56
+ const CodexModelsCacheSchema = z.object({
57
+ models: z.array(
58
+ z.looseObject({
59
+ slug: z.string(),
60
+ visibility: z.string().optional(),
61
+ supported_reasoning_levels: z.array(z.looseObject({ effort: z.string() })).default([]),
62
+ }),
63
+ ),
64
+ });
65
+
66
+ const codexModelsCachePath = (env: NodeJS.ProcessEnv = process.env): string =>
67
+ join(env.CODEX_HOME ?? join(homedir(), ".codex"), "models_cache.json");
68
+
69
+ export const parseCodexModelsCache = (text: string): LiveHarnessModel[] | undefined => {
70
+ const parsed = CodexModelsCacheSchema.safeParse(tryParseJson(text));
71
+ if (!parsed.success) return undefined;
72
+ return parsed.data.models
73
+ .filter((model) => model.visibility !== "hide")
74
+ .map((model) => ({
75
+ id: model.slug,
76
+ efforts: model.supported_reasoning_levels.map((level) => level.effort as ReasoningEffort),
77
+ }));
78
+ };
79
+
80
+ const readCodexLiveModels = async (
81
+ path = codexModelsCachePath(),
82
+ ): Promise<LiveHarnessModel[] | undefined> => {
83
+ try {
84
+ return parseCodexModelsCache(await readFile(path, "utf8"));
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ };
89
+
90
+ export const parseCopilotModelValues = (helpText: string): LiveHarnessModel[] | undefined => {
91
+ const lines = helpText.split("\n");
92
+ const start = lines.findIndex((line) => /^\s*`model`:/.test(line));
93
+ if (start < 0) return undefined;
94
+ const models: LiveHarnessModel[] = [];
95
+ for (const line of lines.slice(start + 1)) {
96
+ const match = /^\s*-\s*"([^"]+)"\s*$/.exec(line);
97
+ if (!match?.[1]) break;
98
+ models.push({ id: match[1] });
99
+ }
100
+ return models.length > 0 ? models : undefined;
101
+ };
102
+
103
+ const readCopilotLiveModels = async (
104
+ command = "copilot",
105
+ ): Promise<LiveHarnessModel[] | undefined> => {
106
+ try {
107
+ const result = await exec(command, ["help", "config"], { timeoutMs: 15_000 });
108
+ if (result.exitCode !== 0 || result.timedOut) return undefined;
109
+ return parseCopilotModelValues(result.stdout);
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ };
114
+
115
+ export type HarnessCatalogProbe = {
116
+ catalog: ClosedHarnessCatalog;
117
+ source: string;
118
+ read: (command: string | undefined) => Promise<LiveHarnessModel[] | undefined>;
119
+ };
120
+
121
+ export const harnessCatalogProbes: readonly HarnessCatalogProbe[] = [
122
+ {
123
+ catalog: codexCatalog,
124
+ source: "models_cache.json",
125
+ read: () => readCodexLiveModels(),
126
+ },
127
+ {
128
+ catalog: copilotCatalog,
129
+ source: "help config",
130
+ read: (command) => readCopilotLiveModels(command),
131
+ },
132
+ ];
@@ -6,6 +6,8 @@ import {
6
6
  type AnswererRef,
7
7
  bindSubjectRoot,
8
8
  type DriverRef,
9
+ driverConfigOf,
10
+ driverNameOf,
9
11
  type EvalCase,
10
12
  type GradingAssertionEntry,
11
13
  type GradingReport,
@@ -13,6 +15,7 @@ import {
13
15
  type HarnessModelConfig,
14
16
  type LifecycleHooksPlugin,
15
17
  mergeHarnessModelConfig,
18
+ mergePluginConfig,
16
19
  type Registry,
17
20
  type RunMetadata,
18
21
  type RunObserver,
@@ -142,6 +145,12 @@ export type RunRequest = {
142
145
  subjectRoot?: string | undefined;
143
146
  subjectHash?: string | undefined;
144
147
  profileId?: string | undefined;
148
+ /**
149
+ * Driver bound by a harness permutation. It replaces the project default;
150
+ * a case pinned to another driver is rejected rather than silently rerouted.
151
+ */
152
+ driver?: DriverRef | undefined;
153
+ permutationId?: string | undefined;
145
154
  experimentId?: string | undefined;
146
155
  variantId?: string | undefined;
147
156
  candidateId?: string | undefined;
@@ -200,7 +209,13 @@ export type SuiteObserver = RunObserver & {
200
209
  * `caseId` is the canonical id runs are filed under when the case declares
201
210
  * none — display decorations (a `--loop` suffix) must not fork run history.
202
211
  */
203
- export type SuiteItem = { path: string; id: string; caseId?: string | undefined };
212
+ export type SuiteItem = {
213
+ path: string;
214
+ id: string;
215
+ caseId?: string | undefined;
216
+ driver?: DriverRef | undefined;
217
+ permutationId?: string | undefined;
218
+ };
204
219
 
205
220
  const resolveCaseId = (casePath: string, evalCase: EvalCase, fallback?: string): string => {
206
221
  if (evalCase.id) return evalCase.id;
@@ -211,39 +226,29 @@ const resolveCaseId = (casePath: string, evalCase: EvalCase, fallback?: string):
211
226
  return ext ? base.slice(0, -ext.length) : base;
212
227
  };
213
228
 
214
- const driverNameOf = (ref: DriverRef): string => (typeof ref === "string" ? ref : ref.name);
215
-
216
- const isRecord = (value: unknown): value is Record<string, unknown> =>
217
- Boolean(value) && typeof value === "object" && !Array.isArray(value);
218
-
219
- /** Merge plugin config recursively so nested Codex `config`/`env` overrides stay additive. */
220
- const mergePluginConfig = (base: unknown, local: unknown): unknown => {
221
- if (!isRecord(base) || !isRecord(local)) return local ?? base;
222
- const merged: Record<string, unknown> = { ...base };
223
- for (const [key, value] of Object.entries(local)) {
224
- merged[key] = key in merged ? mergePluginConfig(merged[key], value) : value;
225
- }
226
- return merged;
227
- };
228
-
229
229
  export const resolveDriverReference = (
230
230
  evalCase: EvalCase,
231
231
  fallback: DriverRef | undefined,
232
+ permutation?: DriverRef | undefined,
232
233
  ): DriverRef => {
233
234
  const local = evalCase.driver;
235
+ if (permutation && local && driverNameOf(local) !== driverNameOf(permutation)) {
236
+ throw new Error(
237
+ `case is pinned to driver '${driverNameOf(local)}' but the run selected '${driverNameOf(permutation)}'`,
238
+ );
239
+ }
240
+ const base = permutation ?? fallback;
234
241
  if (!local) {
235
- if (!fallback) throw new Error("case names no driver and no defaultDriver is configured");
236
- return fallback;
242
+ if (!base) throw new Error("case names no driver and no defaultDriver is configured");
243
+ return base;
237
244
  }
238
- if (!fallback || driverNameOf(local) !== driverNameOf(fallback)) return local;
245
+ if (!base || driverNameOf(local) !== driverNameOf(base)) return local;
239
246
 
240
247
  // Naming the same driver opts into project defaults. A case object only
241
248
  // needs to carry the fields it wants to override (for example a timeout).
242
- const baseConfig = typeof fallback === "object" ? fallback.config : undefined;
243
- const localConfig = typeof local === "object" ? local.config : undefined;
244
249
  return {
245
250
  name: driverNameOf(local),
246
- config: mergePluginConfig(baseConfig, localConfig),
251
+ config: mergePluginConfig(driverConfigOf(base), driverConfigOf(local)),
247
252
  };
248
253
  };
249
254
 
@@ -264,9 +269,6 @@ export const resolveAnswererReference = (
264
269
 
265
270
  const answererNameOf = (ref: AnswererRef): string => (typeof ref === "string" ? ref : ref.name);
266
271
 
267
- const driverConfigOf = (ref: DriverRef): unknown =>
268
- typeof ref === "object" ? ref.config : undefined;
269
-
270
272
  const answererConfigOf = (ref: AnswererRef): unknown =>
271
273
  typeof ref === "object" ? ref.config : undefined;
272
274
 
@@ -352,7 +354,7 @@ export class Orchestrator {
352
354
  registry,
353
355
  );
354
356
 
355
- const driverRef = resolveDriverReference(evalCase, this.defaultDriver);
357
+ const driverRef = resolveDriverReference(evalCase, this.defaultDriver, request.driver);
356
358
  const driverName = driverNameOf(driverRef);
357
359
  const answererRef = resolveAnswererReference(
358
360
  evalCase,
@@ -422,6 +424,7 @@ export class Orchestrator {
422
424
  validatedAssertionConfigs,
423
425
  caseId,
424
426
  driverName,
427
+ permutationId: request.permutationId,
425
428
  answererName,
426
429
  answererConfig: answererCfg,
427
430
  driverConfigHash: valueHash(driverCfg),
@@ -643,6 +646,8 @@ export class Orchestrator {
643
646
  subjectRoot: opts.subjectRoot,
644
647
  subjectHash: opts.subjectHash,
645
648
  profileId: opts.profileId,
649
+ driver: item.driver,
650
+ permutationId: item.permutationId,
646
651
  experimentId: opts.experimentId,
647
652
  variantId: opts.variantId,
648
653
  candidateId: opts.candidateId,
@@ -154,6 +154,8 @@ export type OpenRunDirOptions = {
154
154
  runsRoot: string;
155
155
  caseId: string;
156
156
  driverName: string;
157
+ /** Files the run under the permutation instead of the bare driver name. */
158
+ permutationId?: string | undefined;
157
159
  startedAt?: Date | undefined;
158
160
  /** Tap on the event stream, called for every appended event. Must not throw. */
159
161
  onEvent?: ((event: RunEvent) => void) | undefined;
@@ -164,7 +166,12 @@ export type OpenRunDirOptions = {
164
166
  export const openRunDir = async (options: OpenRunDirOptions): Promise<OpenedRunDir> => {
165
167
  const ts = isoTimestampSafe(options.startedAt ?? new Date());
166
168
  const slug = slugifyCaseId(options.caseId);
167
- const basePath = join(options.runsRoot, slug, slugifyCaseId(options.driverName), ts);
169
+ const basePath = join(
170
+ options.runsRoot,
171
+ slug,
172
+ slugifyCaseId(options.permutationId ?? options.driverName),
173
+ ts,
174
+ );
168
175
  await mkdir(dirname(basePath), { recursive: true });
169
176
  // Same case+driver+millisecond must not share a dir — their events.jsonl
170
177
  // streams (opened with flags "a") would interleave silently. A bare mkdir
@@ -69,6 +69,7 @@ export type RunWithPluginsArgs = {
69
69
  validatedAssertionConfigs: readonly unknown[];
70
70
  caseId: string;
71
71
  driverName: string;
72
+ permutationId?: string | undefined;
72
73
  answererName: string;
73
74
  answererConfig: unknown;
74
75
  driverConfigHash: string;
@@ -338,6 +339,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
338
339
  runsRoot: resolve(args.request.runsRoot),
339
340
  caseId: args.caseId,
340
341
  driverName: args.driverName,
342
+ permutationId: args.permutationId,
341
343
  startedAt,
342
344
  onEvent: observer?.event,
343
345
  });
@@ -653,6 +655,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
653
655
  ...(this.fixturesHash ? { fixturesHash: this.fixturesHash } : {}),
654
656
  ...(request.subjectHash ? { subjectHash: request.subjectHash } : {}),
655
657
  ...(request.profileId ? { profileId: request.profileId } : {}),
658
+ ...(this.args.permutationId ? { permutationId: this.args.permutationId } : {}),
656
659
  ...(request.experimentId ? { experimentId: request.experimentId } : {}),
657
660
  ...(request.variantId ? { variantId: request.variantId } : {}),
658
661
  ...(request.candidateId ? { candidateId: request.candidateId } : {}),