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

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 (84) hide show
  1. package/lib/assertions/authoring.d.ts +14 -0
  2. package/lib/assertions/authoring.d.ts.map +1 -0
  3. package/lib/assertions/authoring.js +3 -0
  4. package/lib/assertions/authoring.js.map +1 -0
  5. package/lib/authoring.d.ts +1 -0
  6. package/lib/authoring.d.ts.map +1 -1
  7. package/lib/authoring.js +1 -0
  8. package/lib/authoring.js.map +1 -1
  9. package/lib/core/assertion.d.ts +7 -9
  10. package/lib/core/assertion.d.ts.map +1 -1
  11. package/lib/core/assertion.js +13 -0
  12. package/lib/core/assertion.js.map +1 -1
  13. package/lib/core/case-authoring-state.d.ts +12 -0
  14. package/lib/core/case-authoring-state.d.ts.map +1 -0
  15. package/lib/core/case-authoring-state.js +43 -0
  16. package/lib/core/case-authoring-state.js.map +1 -0
  17. package/lib/core/case.d.ts +29 -1
  18. package/lib/core/case.d.ts.map +1 -1
  19. package/lib/core/case.js +14 -6
  20. package/lib/core/case.js.map +1 -1
  21. package/lib/core/driver.d.ts +3 -0
  22. package/lib/core/driver.d.ts.map +1 -1
  23. package/lib/core/driver.js.map +1 -1
  24. package/lib/core/lifecycle-fixtures.d.ts +3 -1
  25. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  26. package/lib/core/lifecycle-fixtures.js +4 -2
  27. package/lib/core/lifecycle-fixtures.js.map +1 -1
  28. package/lib/core/lifecycle-hooks.d.ts +9 -0
  29. package/lib/core/lifecycle-hooks.d.ts.map +1 -1
  30. package/lib/core/lifecycle-hooks.js +8 -2
  31. package/lib/core/lifecycle-hooks.js.map +1 -1
  32. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  33. package/lib/drivers/subprocess/index.js +6 -0
  34. package/lib/drivers/subprocess/index.js.map +1 -1
  35. package/lib/index.d.ts +4 -3
  36. package/lib/index.d.ts.map +1 -1
  37. package/lib/index.js +4 -3
  38. package/lib/index.js.map +1 -1
  39. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  40. package/lib/orchestrator/annotation-store.js +6 -2
  41. package/lib/orchestrator/annotation-store.js.map +1 -1
  42. package/lib/orchestrator/case-extensions.d.ts +6 -0
  43. package/lib/orchestrator/case-extensions.d.ts.map +1 -0
  44. package/lib/orchestrator/case-extensions.js +28 -0
  45. package/lib/orchestrator/case-extensions.js.map +1 -0
  46. package/lib/orchestrator/doctor.d.ts.map +1 -1
  47. package/lib/orchestrator/doctor.js +5 -3
  48. package/lib/orchestrator/doctor.js.map +1 -1
  49. package/lib/orchestrator/loader.d.ts.map +1 -1
  50. package/lib/orchestrator/loader.js +2 -0
  51. package/lib/orchestrator/loader.js.map +1 -1
  52. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  53. package/lib/orchestrator/orchestrator.js +39 -14
  54. package/lib/orchestrator/orchestrator.js.map +1 -1
  55. package/lib/orchestrator/owned-lock.d.ts.map +1 -1
  56. package/lib/orchestrator/owned-lock.js +52 -38
  57. package/lib/orchestrator/owned-lock.js.map +1 -1
  58. package/lib/orchestrator/regrade.d.ts.map +1 -1
  59. package/lib/orchestrator/regrade.js +8 -4
  60. package/lib/orchestrator/regrade.js.map +1 -1
  61. package/lib/orchestrator/run-execution.d.ts +2 -1
  62. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  63. package/lib/orchestrator/run-execution.js +1 -0
  64. package/lib/orchestrator/run-execution.js.map +1 -1
  65. package/package.json +1 -1
  66. package/skills/lux-answerer/SKILL.md +1 -1
  67. package/src/assertions/authoring.ts +79 -0
  68. package/src/authoring.ts +1 -0
  69. package/src/core/assertion.ts +26 -22
  70. package/src/core/case-authoring-state.ts +65 -0
  71. package/src/core/case.ts +57 -6
  72. package/src/core/driver.ts +3 -0
  73. package/src/core/lifecycle-fixtures.ts +7 -2
  74. package/src/core/lifecycle-hooks.ts +16 -2
  75. package/src/drivers/subprocess/index.ts +6 -0
  76. package/src/index.ts +10 -1
  77. package/src/orchestrator/annotation-store.ts +5 -1
  78. package/src/orchestrator/case-extensions.ts +44 -0
  79. package/src/orchestrator/doctor.ts +5 -3
  80. package/src/orchestrator/loader.ts +2 -0
  81. package/src/orchestrator/orchestrator.ts +80 -40
  82. package/src/orchestrator/owned-lock.ts +54 -34
  83. package/src/orchestrator/regrade.ts +8 -7
  84. package/src/orchestrator/run-execution.ts +3 -0
@@ -5,6 +5,7 @@ import { glob } from "tinyglobby";
5
5
  import { z } from "zod";
6
6
  import { RubricConfigSchema } from "../assertions/rubric/index.js";
7
7
  import type { EvalCase, Registry } from "../core/index.js";
8
+ import { registryForEvalCase } from "./case-extensions.js";
8
9
  import { buildRegistryFromConfig, loadLuxConfig, type ResolvedLuxConfig } from "./config.js";
9
10
  import { type DiscoveredCase, type DiscoveryResult, discoverCases } from "./discover.js";
10
11
  import { cliIdentity, ExperimentCampaignPlanSchema, planExperimentCampaign } from "./experiment.js";
@@ -559,12 +560,13 @@ const inspectCase = async (
559
560
  ): Promise<CliTarget[]> => {
560
561
  try {
561
562
  const authored = await loadEvalCase(discovered.path);
563
+ const caseRegistry = registryForEvalCase(registry, authored);
562
564
  const drivers = caseDriverReferences(authored.driver, config);
563
565
  const answerer = authored.answerer ?? config.defaultAnswerer;
564
- validateCaseDrivers(discovered.id, drivers, registry, recorder);
565
- validateCaseAnswerer(discovered.id, answerer, registry, recorder);
566
+ validateCaseDrivers(discovered.id, drivers, caseRegistry, recorder);
567
+ validateCaseAnswerer(discovered.id, answerer, caseRegistry, recorder);
566
568
  return [
567
- ...(await validateCaseAssertions(discovered.id, authored, config, registry, recorder)),
569
+ ...(await validateCaseAssertions(discovered.id, authored, config, caseRegistry, recorder)),
568
570
  ...executionTargetsForCase(discovered.id, drivers, answerer),
569
571
  ];
570
572
  } catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
+ import { transferEvalCaseExtensions } from "../core/case-authoring-state.js";
3
4
  import { type EvalCase, EvalCaseSchema, formatZodIssues } from "../core/index.js";
4
5
 
5
6
  export const loadEvalCase = async (filePath: string): Promise<EvalCase> => {
@@ -15,5 +16,6 @@ export const loadEvalCase = async (filePath: string): Promise<EvalCase> => {
15
16
  if (!result.success) {
16
17
  throw new Error(`Invalid eval case at ${absolute}: ${formatZodIssues(result.error)}`);
17
18
  }
19
+ transferEvalCaseExtensions(mod.default as EvalCase, result.data);
18
20
  return result.data;
19
21
  };
@@ -18,6 +18,12 @@ import {
18
18
  type SuiteLifecycleHookContext,
19
19
  valueHash,
20
20
  } from "../core/index.js";
21
+ import {
22
+ fixturesForEvalCase,
23
+ mergeLifecycleFixtures,
24
+ registryForEvalCase,
25
+ valuesForFixtures,
26
+ } from "./case-extensions.js";
21
27
  import { evaluatorImplementationHash } from "./evaluator-identity.js";
22
28
  import {
23
29
  assertionEvaluatorIdentities,
@@ -476,11 +482,6 @@ export class Orchestrator {
476
482
  }
477
483
 
478
484
  async run(request: RunRequest): Promise<RunResult> {
479
- if (this.lifecycleFixtures.length > 0 && !request.suiteId) {
480
- throw new Error(
481
- "Orchestrator.run() cannot execute configured lifecycle fixtures; use runSuite() for a single case",
482
- );
483
- }
484
485
  if (!request.case && !request.casePath) {
485
486
  throw new Error("RunRequest requires either 'case' or 'casePath'");
486
487
  }
@@ -489,11 +490,24 @@ export class Orchestrator {
489
490
  }
490
491
  const casePath = request.casePath ? resolve(request.casePath) : INLINE_CASE_PATH;
491
492
  const authoredCase = request.case ?? (await loadEvalCase(casePath));
493
+ const lifecycleFixtures = mergeLifecycleFixtures(
494
+ this.lifecycleFixtures,
495
+ fixturesForEvalCase(authoredCase),
496
+ );
497
+ if (
498
+ lifecycleFixtures.some(({ setupSuite, setupCase }) => setupSuite || setupCase) &&
499
+ !request.suiteId
500
+ ) {
501
+ throw new Error(
502
+ "Orchestrator.run() cannot execute lifecycle fixtures; use runSuite() for a single case",
503
+ );
504
+ }
505
+ const registry = registryForEvalCase(this.registry, authoredCase);
492
506
  const evalCase = bindSubjectRoot(authoredCase, request.subjectRoot);
493
507
  const caseId = resolveCaseId(casePath, evalCase, request.caseId);
494
508
  const validatedAssertionConfigs = await validateAssertionPlan(
495
509
  evalCase.assertions ?? [],
496
- this.registry,
510
+ registry,
497
511
  );
498
512
 
499
513
  const driverRef = resolveDriverReference(evalCase, this.defaultDriver);
@@ -506,11 +520,11 @@ export class Orchestrator {
506
520
  );
507
521
  const answererName = answererNameOf(answererRef);
508
522
 
509
- const driverPlugin = this.registry.getDriver(driverName);
523
+ const driverPlugin = registry.getDriver(driverName);
510
524
  if (!driverPlugin) {
511
525
  throw new Error(`no driver registered for '${driverName}'`);
512
526
  }
513
- const answererPlugin = this.registry.getAnswerer(answererName);
527
+ const answererPlugin = registry.getAnswerer(answererName);
514
528
  if (!answererPlugin) {
515
529
  throw new Error(`no answerer registered for '${answererName}'`);
516
530
  }
@@ -544,7 +558,7 @@ export class Orchestrator {
544
558
  try {
545
559
  const assertionImplementations = assertionEvaluatorIdentities(
546
560
  authoredCase.assertions ?? [],
547
- this.registry,
561
+ registry,
548
562
  );
549
563
  const validatedAssertionsHash = valueHash(validatedAssertionConfigs);
550
564
  const implementationHash = await this.evaluatorImplementationIdentity();
@@ -555,33 +569,37 @@ export class Orchestrator {
555
569
  validatedAssertionsHash,
556
570
  graderContext: this.harness,
557
571
  });
558
- return await this.runWithPlugins({
559
- request,
560
- casePath,
561
- authoredCase,
562
- evalCase,
563
- validatedAssertionConfigs,
564
- caseId,
565
- driverName,
566
- answererName,
567
- answererConfig: answererCfg,
568
- driverConfigHash: valueHash(driverCfg),
569
- answererConfigHash: valueHash(answererCfg),
570
- validatedAssertionsHash,
571
- graderConfigHash: graderHash,
572
- evaluatorConfigHash: valueHash({
573
- luxVersion: LUX_VERSION,
574
- implementationHash,
575
- harness: this.harness,
576
- driver: { name: driverPlugin.name, create: driverPlugin.create },
577
- answerer: { name: answererPlugin.name, create: answererPlugin.create },
578
- assertions: assertionImplementations,
579
- lifecycleFixtures: this.lifecycleFixtures,
572
+ return await this.runWithPlugins(
573
+ {
574
+ request,
575
+ casePath,
576
+ authoredCase,
577
+ evalCase,
578
+ validatedAssertionConfigs,
579
+ caseId,
580
+ driverName,
581
+ answererName,
582
+ answererConfig: answererCfg,
583
+ driverConfigHash: valueHash(driverCfg),
584
+ answererConfigHash: valueHash(answererCfg),
580
585
  validatedAssertionsHash,
581
- }),
582
- driver,
583
- answerer,
584
- });
586
+ graderConfigHash: graderHash,
587
+ evaluatorConfigHash: valueHash({
588
+ luxVersion: LUX_VERSION,
589
+ implementationHash,
590
+ harness: this.harness,
591
+ driver: { name: driverPlugin.name, create: driverPlugin.create },
592
+ answerer: { name: answererPlugin.name, create: answererPlugin.create },
593
+ assertions: assertionImplementations,
594
+ lifecycleFixtures,
595
+ validatedAssertionsHash,
596
+ }),
597
+ fixtureValues: valuesForFixtures(lifecycleFixtures),
598
+ driver,
599
+ answerer,
600
+ },
601
+ registry,
602
+ );
585
603
  } finally {
586
604
  await closeQuietly(() => driver.close(), `driver '${driverName}'`);
587
605
  await closeQuietly(() => answerer.close(), `answerer '${answererName}'`);
@@ -628,6 +646,20 @@ export class Orchestrator {
628
646
  const observer = opts.observer;
629
647
  const requestedConcurrency = validateSuiteConcurrency(opts.concurrency);
630
648
  const concurrency = Math.min(requestedConcurrency, Math.max(items.length, 1));
649
+ const authoredCases = await Promise.allSettled(items.map((item) => loadEvalCase(item.path)));
650
+ const localFixtures = authoredCases.map((result) =>
651
+ result.status === "fulfilled" ? fixturesForEvalCase(result.value) : [],
652
+ );
653
+ const lifecycleFixtures = mergeLifecycleFixtures(this.lifecycleFixtures, ...localFixtures);
654
+ const globalFixtureNames = new Set(this.lifecycleFixtures.map((fixture) => fixture.name));
655
+ const itemIndexesByFixture = new Map<string, number[]>();
656
+ for (const [index, fixtures] of localFixtures.entries()) {
657
+ for (const fixture of fixtures) {
658
+ const indexes = itemIndexesByFixture.get(fixture.name) ?? [];
659
+ indexes.push(index);
660
+ itemIndexesByFixture.set(fixture.name, indexes);
661
+ }
662
+ }
631
663
  const suite = await openSuiteRun({
632
664
  runsRoot: opts.runsRoot,
633
665
  concurrency,
@@ -639,7 +671,7 @@ export class Orchestrator {
639
671
  ...(opts.variantId ? { variantId: opts.variantId } : {}),
640
672
  ...(opts.candidateId ? { candidateId: opts.candidateId } : {}),
641
673
  ...(opts.subjectHash ? { subjectHash: opts.subjectHash } : {}),
642
- fixtures: this.lifecycleFixtures.map(({ name, pluginVersion }) => ({
674
+ fixtures: lifecycleFixtures.map(({ name, pluginVersion }) => ({
643
675
  name,
644
676
  pluginVersion,
645
677
  })),
@@ -677,9 +709,16 @@ export class Orchestrator {
677
709
  if (!opts.abortSignal?.aborted) {
678
710
  try {
679
711
  cleanupSuiteHooks = await setupLifecycleHooks(
680
- this.lifecycleFixtures,
712
+ lifecycleFixtures,
681
713
  "suite",
682
- (hook) => hook.setupSuite?.(hookContext),
714
+ (hook) => {
715
+ if (globalFixtureNames.has(hook.name)) return hook.setupSuite?.(hookContext);
716
+ const indexes = itemIndexesByFixture.get(hook.name) ?? [];
717
+ return hook.setupSuite?.({
718
+ ...hookContext,
719
+ items: indexes.flatMap((index) => hookContext.items[index] ?? []),
720
+ });
721
+ },
683
722
  opts.abortSignal,
684
723
  );
685
724
  } catch (error) {
@@ -718,7 +757,7 @@ export class Orchestrator {
718
757
  let cleanupCaseHooks: () => void | Promise<void>;
719
758
  try {
720
759
  cleanupCaseHooks = await setupLifecycleHooks(
721
- this.lifecycleFixtures,
760
+ mergeLifecycleFixtures(this.lifecycleFixtures, localFixtures[index] ?? []),
722
761
  "case",
723
762
  (hook) =>
724
763
  hook.setupCase?.({
@@ -906,11 +945,12 @@ export class Orchestrator {
906
945
 
907
946
  private runWithPlugins(
908
947
  args: Omit<RunWithPluginsArgs, "luxVersion" | "registry">,
948
+ registry: Registry = this.registry,
909
949
  ): Promise<RunResult> {
910
950
  return executeRunWithPlugins({
911
951
  ...args,
912
952
  luxVersion: LUX_VERSION,
913
- registry: this.registry,
953
+ registry,
914
954
  });
915
955
  }
916
956
  }
@@ -1,8 +1,6 @@
1
- import { randomInt, randomUUID } from "node:crypto";
2
- import { link, lstat, open, readdir, readFile, rm, unlink } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, lstat, open, readFile, rm, unlink } from "node:fs/promises";
3
3
  import { hostname } from "node:os";
4
- import { basename, dirname, join } from "node:path";
5
- import { setTimeout as delay } from "node:timers/promises";
6
4
 
7
5
  export type LockRecoveryClaim = {
8
6
  path: string;
@@ -60,36 +58,54 @@ const linkedClaim = (lockPath: string, claimPath: string): LockRecoveryClaim =>
60
58
  release: () => rm(claimPath, { force: true }),
61
59
  });
62
60
 
63
- const recoveryHost = Buffer.from(hostname()).toString("base64url");
61
+ type RecoveryMutexOwner = {
62
+ pid: number;
63
+ hostname: string;
64
+ lockToken: string;
65
+ };
64
66
 
65
- const localClaimProcessIsDead = (prefix: string, name: string): boolean => {
66
- const [claimHost, rawPid, id, extra] = name.slice(prefix.length).split(".");
67
- if (claimHost !== recoveryHost || !rawPid || !id || extra !== undefined) return false;
67
+ const recoveryOwnerIsDead = (owner: RecoveryMutexOwner): boolean => {
68
+ if (owner.hostname !== hostname()) return false;
68
69
  try {
69
- process.kill(Number(rawPid), 0);
70
+ process.kill(owner.pid, 0);
70
71
  return false;
71
72
  } catch (error) {
72
73
  return (error as NodeJS.ErrnoException).code === "ESRCH";
73
74
  }
74
75
  };
75
76
 
76
- const hasCompetingRecoveryClaim = async (lockPath: string, claimPath: string): Promise<boolean> => {
77
- const claim = await lstat(claimPath);
78
- const directory = dirname(lockPath);
79
- const claimName = basename(claimPath);
80
- const prefix = `${basename(lockPath)}.recovering-`;
81
- for (const name of await readdir(directory)) {
82
- if (name === claimName || !name.startsWith(prefix)) continue;
83
- const peerPath = join(directory, name);
84
- const peer = await missingAsNull(lstat(peerPath));
85
- if (!peer || peer.dev !== claim.dev || peer.ino !== claim.ino) continue;
86
- if (localClaimProcessIsDead(prefix, name)) {
87
- await unlinkIfExists(peerPath);
88
- continue;
77
+ const acquireRecoveryMutex = async (
78
+ lockPath: string,
79
+ ): Promise<{ path: string; lockToken: string } | null> => {
80
+ const path = `${lockPath}.recovery`;
81
+ while (true) {
82
+ try {
83
+ const owner = await publishOwnedLock(path, {
84
+ pid: process.pid,
85
+ hostname: hostname(),
86
+ });
87
+ return { path, lockToken: owner.lockToken };
88
+ } catch (error) {
89
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
89
90
  }
90
- return true;
91
+ let owner: RecoveryMutexOwner;
92
+ try {
93
+ const value = JSON.parse(await readFile(path, "utf8")) as Partial<RecoveryMutexOwner>;
94
+ if (
95
+ !Number.isSafeInteger(value.pid) ||
96
+ typeof value.hostname !== "string" ||
97
+ typeof value.lockToken !== "string"
98
+ ) {
99
+ return null;
100
+ }
101
+ owner = value as RecoveryMutexOwner;
102
+ } catch (error) {
103
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
104
+ return null;
105
+ }
106
+ if (!recoveryOwnerIsDead(owner)) return null;
107
+ await releaseOwnedLock(path, owner.lockToken);
91
108
  }
92
- return false;
93
109
  };
94
110
 
95
111
  /**
@@ -97,24 +113,28 @@ const hasCompetingRecoveryClaim = async (lockPath: string, claimPath: string): P
97
113
  * Foreign claims stay conservative because foreign lock owners do too.
98
114
  */
99
115
  export const claimLockForRecovery = async (lockPath: string): Promise<LockRecoveryClaim | null> => {
100
- const claimPath = `${lockPath}.recovering-${recoveryHost}.${process.pid}.${randomUUID()}`;
116
+ const mutex = await acquireRecoveryMutex(lockPath);
117
+ if (!mutex) return null;
118
+ const claimPath = `${lockPath}.recovering-${mutex.lockToken}`;
101
119
  try {
102
120
  await link(lockPath, claimPath);
103
121
  } catch (error) {
104
122
  const code = (error as NodeJS.ErrnoException).code;
123
+ await releaseOwnedLock(mutex.path, mutex.lockToken);
105
124
  if (code === "ENOENT") return null;
106
125
  throw error;
107
126
  }
108
127
  const claim = linkedClaim(lockPath, claimPath);
109
- try {
110
- if (!(await hasCompetingRecoveryClaim(lockPath, claimPath))) return claim;
111
- } catch (error) {
112
- await claim.release();
113
- throw error;
114
- }
115
- await claim.release();
116
- await delay(randomInt(1, 8));
117
- return null;
128
+ return {
129
+ ...claim,
130
+ release: async () => {
131
+ try {
132
+ await claim.release();
133
+ } finally {
134
+ await releaseOwnedLock(mutex.path, mutex.lockToken);
135
+ }
136
+ },
137
+ };
118
138
  };
119
139
 
120
140
  /** Remove only the canonical lock that still carries this acquisition token. */
@@ -33,6 +33,7 @@ import {
33
33
  readRunArtifactBytes,
34
34
  valueHash,
35
35
  } from "../core/index.js";
36
+ import { registryForEvalCase } from "./case-extensions.js";
36
37
  import { evaluatorImplementationHash } from "./evaluator-identity.js";
37
38
  import { parsePersistedJson, readBoundedHandle, safeReadJson } from "./fs-read.js";
38
39
  import {
@@ -526,18 +527,18 @@ export const regradeRun = async (options: {
526
527
  now?: Date;
527
528
  }): Promise<{ path: string; snapshot: GradingSnapshot }> => {
528
529
  const run = await loadFrozenRegradableRun(options.runDir, options.case);
530
+ const registry = options.case
531
+ ? registryForEvalCase(options.registry, options.case)
532
+ : options.registry;
529
533
  try {
530
534
  const evidenceHash = await gradingEvidenceHash(run);
531
535
  const validatedAssertionConfigs = await validateAssertionPlan(
532
536
  run.case.assertions ?? [],
533
- options.registry,
537
+ registry,
534
538
  );
535
539
  const validatedAssertionsHash = valueHash(validatedAssertionConfigs);
536
- const assertionEvaluators = assertionEvaluatorIdentities(
537
- run.case.assertions ?? [],
538
- options.registry,
539
- );
540
- const assertionPlugins = assertionPluginIdentities(run.case.assertions ?? [], options.registry);
540
+ const assertionEvaluators = assertionEvaluatorIdentities(run.case.assertions ?? [], registry);
541
+ const assertionPlugins = assertionPluginIdentities(run.case.assertions ?? [], registry);
541
542
  const graderHash = graderConfigHash({
542
543
  luxVersion: LUX_VERSION,
543
544
  implementationHash: await currentEvaluatorImplementationHash(),
@@ -547,7 +548,7 @@ export const regradeRun = async (options: {
547
548
  });
548
549
  const report: GradingReport = await gradeRun(
549
550
  run,
550
- options.registry,
551
+ registry,
551
552
  undefined,
552
553
  options.abortSignal,
553
554
  undefined,
@@ -13,6 +13,7 @@ import {
13
13
  type EvalCase,
14
14
  emptyUsage,
15
15
  type FixturesRef,
16
+ type FixtureValueBinding,
16
17
  type GradingReport,
17
18
  guardRunObserver,
18
19
  inspectRunArtifact,
@@ -70,6 +71,7 @@ export type RunWithPluginsArgs = {
70
71
  validatedAssertionsHash: string;
71
72
  graderConfigHash: string;
72
73
  evaluatorConfigHash: string;
74
+ fixtureValues: readonly FixtureValueBinding[];
73
75
  luxVersion: string;
74
76
  registry: Registry;
75
77
  driver: AgentDriver;
@@ -360,6 +362,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
360
362
  caseId: this.args.caseId,
361
363
  prompt: evalCase.prompt,
362
364
  fixtures: staged?.fixtures,
365
+ ...(this.args.fixtureValues.length > 0 ? { fixtureValues: this.args.fixtureValues } : {}),
363
366
  runsRoot: resolve(request.runsRoot),
364
367
  runDir: this.runDir.path,
365
368
  artifactsDir: this.runDir.artifactsDir,