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

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 (256) hide show
  1. package/lib/answerers/persona/index.d.ts +2 -1
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/answerers/persona/index.js +19 -5
  4. package/lib/answerers/persona/index.js.map +1 -1
  5. package/lib/answerers/terminal/index.js +1 -1
  6. package/lib/answerers/terminal/index.js.map +1 -1
  7. package/lib/assertions/authoring.d.ts +14 -0
  8. package/lib/assertions/authoring.d.ts.map +1 -0
  9. package/lib/assertions/authoring.js +3 -0
  10. package/lib/assertions/authoring.js.map +1 -0
  11. package/lib/assertions/core/helpers.d.ts +2 -7
  12. package/lib/assertions/core/helpers.d.ts.map +1 -1
  13. package/lib/assertions/core/helpers.js +2 -6
  14. package/lib/assertions/core/helpers.js.map +1 -1
  15. package/lib/assertions/core/index.d.ts.map +1 -1
  16. package/lib/assertions/core/index.js +2 -1
  17. package/lib/assertions/core/index.js.map +1 -1
  18. package/lib/assertions/core/resource-checks.d.ts +19 -0
  19. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  20. package/lib/assertions/core/resource-checks.js +32 -0
  21. package/lib/assertions/core/resource-checks.js.map +1 -1
  22. package/lib/authoring.d.ts +1 -0
  23. package/lib/authoring.d.ts.map +1 -1
  24. package/lib/authoring.js +1 -0
  25. package/lib/authoring.js.map +1 -1
  26. package/lib/cli/program.d.ts.map +1 -1
  27. package/lib/cli/program.js +12 -67
  28. package/lib/cli/program.js.map +1 -1
  29. package/lib/cli/render/reporter.d.ts.map +1 -1
  30. package/lib/cli/render/reporter.js +16 -3
  31. package/lib/cli/render/reporter.js.map +1 -1
  32. package/lib/cli/run-options.d.ts +1 -0
  33. package/lib/cli/run-options.d.ts.map +1 -1
  34. package/lib/cli/run-options.js.map +1 -1
  35. package/lib/cli/view.d.ts +13 -0
  36. package/lib/cli/view.d.ts.map +1 -0
  37. package/lib/cli/view.js +126 -0
  38. package/lib/cli/view.js.map +1 -0
  39. package/lib/core/action-events.d.ts +29 -0
  40. package/lib/core/action-events.d.ts.map +1 -0
  41. package/lib/core/action-events.js +51 -0
  42. package/lib/core/action-events.js.map +1 -0
  43. package/lib/core/agent-activity.d.ts +49 -0
  44. package/lib/core/agent-activity.d.ts.map +1 -0
  45. package/lib/core/agent-activity.js +206 -0
  46. package/lib/core/agent-activity.js.map +1 -0
  47. package/lib/core/agent-events.d.ts +26 -0
  48. package/lib/core/agent-events.d.ts.map +1 -0
  49. package/lib/core/agent-events.js +13 -0
  50. package/lib/core/agent-events.js.map +1 -0
  51. package/lib/core/artifact-evidence.d.ts +8 -0
  52. package/lib/core/artifact-evidence.d.ts.map +1 -1
  53. package/lib/core/artifact-evidence.js +36 -4
  54. package/lib/core/artifact-evidence.js.map +1 -1
  55. package/lib/core/assertion.d.ts +19 -9
  56. package/lib/core/assertion.d.ts.map +1 -1
  57. package/lib/core/assertion.js +26 -0
  58. package/lib/core/assertion.js.map +1 -1
  59. package/lib/core/case-authoring-state.d.ts +12 -0
  60. package/lib/core/case-authoring-state.d.ts.map +1 -0
  61. package/lib/core/case-authoring-state.js +43 -0
  62. package/lib/core/case-authoring-state.js.map +1 -0
  63. package/lib/core/case.d.ts +29 -1
  64. package/lib/core/case.d.ts.map +1 -1
  65. package/lib/core/case.js +14 -6
  66. package/lib/core/case.js.map +1 -1
  67. package/lib/core/driver-metrics.d.ts +98 -0
  68. package/lib/core/driver-metrics.d.ts.map +1 -0
  69. package/lib/core/driver-metrics.js +36 -0
  70. package/lib/core/driver-metrics.js.map +1 -0
  71. package/lib/core/driver.d.ts +3 -0
  72. package/lib/core/driver.d.ts.map +1 -1
  73. package/lib/core/driver.js.map +1 -1
  74. package/lib/core/events.d.ts +36 -0
  75. package/lib/core/events.d.ts.map +1 -1
  76. package/lib/core/events.js +12 -2
  77. package/lib/core/events.js.map +1 -1
  78. package/lib/core/experiment.d.ts +19 -0
  79. package/lib/core/experiment.d.ts.map +1 -1
  80. package/lib/core/experiment.js +2 -0
  81. package/lib/core/experiment.js.map +1 -1
  82. package/lib/core/index.d.ts +4 -0
  83. package/lib/core/index.d.ts.map +1 -1
  84. package/lib/core/index.js +4 -0
  85. package/lib/core/index.js.map +1 -1
  86. package/lib/core/lifecycle-fixtures.d.ts +3 -1
  87. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  88. package/lib/core/lifecycle-fixtures.js +4 -2
  89. package/lib/core/lifecycle-fixtures.js.map +1 -1
  90. package/lib/core/lifecycle-hooks.d.ts +9 -0
  91. package/lib/core/lifecycle-hooks.d.ts.map +1 -1
  92. package/lib/core/lifecycle-hooks.js +8 -2
  93. package/lib/core/lifecycle-hooks.js.map +1 -1
  94. package/lib/core/run.d.ts +150 -0
  95. package/lib/core/run.d.ts.map +1 -1
  96. package/lib/core/run.js +9 -0
  97. package/lib/core/run.js.map +1 -1
  98. package/lib/core/wire.d.ts +12 -0
  99. package/lib/core/wire.d.ts.map +1 -1
  100. package/lib/core/wire.js +5 -1
  101. package/lib/core/wire.js.map +1 -1
  102. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
  103. package/lib/drivers/claude-code/parse-events.js +73 -8
  104. package/lib/drivers/claude-code/parse-events.js.map +1 -1
  105. package/lib/drivers/codex/app-events.d.ts +9 -2
  106. package/lib/drivers/codex/app-events.d.ts.map +1 -1
  107. package/lib/drivers/codex/app-events.js +93 -12
  108. package/lib/drivers/codex/app-events.js.map +1 -1
  109. package/lib/drivers/codex/index.d.ts.map +1 -1
  110. package/lib/drivers/codex/index.js +10 -14
  111. package/lib/drivers/codex/index.js.map +1 -1
  112. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  113. package/lib/drivers/subprocess/index.js +6 -0
  114. package/lib/drivers/subprocess/index.js.map +1 -1
  115. package/lib/index.d.ts +10 -6
  116. package/lib/index.d.ts.map +1 -1
  117. package/lib/index.js +10 -6
  118. package/lib/index.js.map +1 -1
  119. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  120. package/lib/orchestrator/annotation-store.js +6 -2
  121. package/lib/orchestrator/annotation-store.js.map +1 -1
  122. package/lib/orchestrator/case-extensions.d.ts +6 -0
  123. package/lib/orchestrator/case-extensions.d.ts.map +1 -0
  124. package/lib/orchestrator/case-extensions.js +28 -0
  125. package/lib/orchestrator/case-extensions.js.map +1 -0
  126. package/lib/orchestrator/compare.d.ts +13 -1
  127. package/lib/orchestrator/compare.d.ts.map +1 -1
  128. package/lib/orchestrator/compare.js +12 -0
  129. package/lib/orchestrator/compare.js.map +1 -1
  130. package/lib/orchestrator/config.d.ts.map +1 -1
  131. package/lib/orchestrator/config.js +0 -2
  132. package/lib/orchestrator/config.js.map +1 -1
  133. package/lib/orchestrator/doctor.d.ts.map +1 -1
  134. package/lib/orchestrator/doctor.js +5 -3
  135. package/lib/orchestrator/doctor.js.map +1 -1
  136. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  137. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  138. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  139. package/lib/orchestrator/experiment-evaluation.js +17 -0
  140. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  141. package/lib/orchestrator/experiment-report.d.ts +19 -0
  142. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  143. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  144. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  145. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  146. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  147. package/lib/orchestrator/grading.d.ts +2 -1
  148. package/lib/orchestrator/grading.d.ts.map +1 -1
  149. package/lib/orchestrator/grading.js +16 -4
  150. package/lib/orchestrator/grading.js.map +1 -1
  151. package/lib/orchestrator/index.d.ts +1 -1
  152. package/lib/orchestrator/index.d.ts.map +1 -1
  153. package/lib/orchestrator/index.js +1 -1
  154. package/lib/orchestrator/index.js.map +1 -1
  155. package/lib/orchestrator/list-runs.d.ts +7 -1
  156. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  157. package/lib/orchestrator/list-runs.js +50 -6
  158. package/lib/orchestrator/list-runs.js.map +1 -1
  159. package/lib/orchestrator/loader.d.ts.map +1 -1
  160. package/lib/orchestrator/loader.js +2 -0
  161. package/lib/orchestrator/loader.js.map +1 -1
  162. package/lib/orchestrator/orchestrator.d.ts +3 -0
  163. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  164. package/lib/orchestrator/orchestrator.js +42 -14
  165. package/lib/orchestrator/orchestrator.js.map +1 -1
  166. package/lib/orchestrator/owned-lock.d.ts.map +1 -1
  167. package/lib/orchestrator/owned-lock.js +52 -38
  168. package/lib/orchestrator/owned-lock.js.map +1 -1
  169. package/lib/orchestrator/regrade.d.ts +6 -0
  170. package/lib/orchestrator/regrade.d.ts.map +1 -1
  171. package/lib/orchestrator/regrade.js +8 -4
  172. package/lib/orchestrator/regrade.js.map +1 -1
  173. package/lib/orchestrator/run-execution.d.ts +2 -1
  174. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  175. package/lib/orchestrator/run-execution.js +20 -5
  176. package/lib/orchestrator/run-execution.js.map +1 -1
  177. package/lib/orchestrator/suite-compare.d.ts +34 -0
  178. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  179. package/lib/orchestrator/suite-summary.d.ts +68 -0
  180. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  181. package/lib/orchestrator/suite-summary.js +45 -7
  182. package/lib/orchestrator/suite-summary.js.map +1 -1
  183. package/lib/previewer/artifacts.d.ts +25 -0
  184. package/lib/previewer/artifacts.d.ts.map +1 -0
  185. package/lib/previewer/artifacts.js +180 -0
  186. package/lib/previewer/artifacts.js.map +1 -0
  187. package/lib/previewer/server.d.ts +105 -0
  188. package/lib/previewer/server.d.ts.map +1 -0
  189. package/lib/previewer/server.js +542 -0
  190. package/lib/previewer/server.js.map +1 -0
  191. package/package.json +2 -2
  192. package/skills/lux/SKILL.md +7 -3
  193. package/skills/lux/references/cli.md +25 -2
  194. package/skills/lux/references/eval-authoring.md +158 -4
  195. package/skills/lux-answerer/SKILL.md +1 -1
  196. package/src/answerers/persona/index.ts +20 -4
  197. package/src/answerers/terminal/index.ts +1 -1
  198. package/src/assertions/authoring.ts +81 -0
  199. package/src/assertions/core/helpers.ts +3 -6
  200. package/src/assertions/core/index.ts +2 -0
  201. package/src/assertions/core/resource-checks.ts +35 -0
  202. package/src/authoring.ts +1 -0
  203. package/src/cli/program.ts +14 -93
  204. package/src/cli/render/reporter.ts +18 -3
  205. package/src/cli/run-options.ts +1 -0
  206. package/src/cli/view.ts +168 -0
  207. package/src/core/action-events.ts +59 -0
  208. package/src/core/agent-activity.ts +236 -0
  209. package/src/core/agent-events.ts +17 -0
  210. package/src/core/artifact-evidence.ts +47 -4
  211. package/src/core/assertion.ts +41 -22
  212. package/src/core/case-authoring-state.ts +65 -0
  213. package/src/core/case.ts +57 -6
  214. package/src/core/driver-metrics.ts +51 -0
  215. package/src/core/driver.ts +3 -0
  216. package/src/core/events.ts +14 -2
  217. package/src/core/experiment.ts +2 -0
  218. package/src/core/index.ts +4 -0
  219. package/src/core/lifecycle-fixtures.ts +7 -2
  220. package/src/core/lifecycle-hooks.ts +16 -2
  221. package/src/core/run.ts +9 -0
  222. package/src/core/wire.ts +5 -1
  223. package/src/drivers/claude-code/parse-events.ts +98 -12
  224. package/src/drivers/codex/app-events.ts +116 -16
  225. package/src/drivers/codex/index.ts +16 -14
  226. package/src/drivers/subprocess/index.ts +6 -0
  227. package/src/index.ts +50 -6
  228. package/src/orchestrator/annotation-store.ts +5 -1
  229. package/src/orchestrator/case-extensions.ts +44 -0
  230. package/src/orchestrator/compare.ts +18 -0
  231. package/src/orchestrator/config.ts +0 -2
  232. package/src/orchestrator/doctor.ts +5 -3
  233. package/src/orchestrator/experiment-evaluation.ts +23 -0
  234. package/src/orchestrator/grading.ts +21 -2
  235. package/src/orchestrator/index.ts +1 -0
  236. package/src/orchestrator/list-runs.ts +66 -5
  237. package/src/orchestrator/loader.ts +2 -0
  238. package/src/orchestrator/orchestrator.ts +86 -40
  239. package/src/orchestrator/owned-lock.ts +54 -34
  240. package/src/orchestrator/regrade.ts +8 -7
  241. package/src/orchestrator/run-execution.ts +24 -3
  242. package/src/orchestrator/suite-summary.ts +101 -8
  243. package/src/previewer/artifacts.ts +221 -0
  244. package/src/previewer/server.ts +765 -0
  245. package/viewer/app.css +2075 -0
  246. package/viewer/app.js +606 -0
  247. package/viewer/artifact-view.js +48 -0
  248. package/viewer/charts.js +86 -0
  249. package/viewer/detail.js +1214 -0
  250. package/viewer/format.js +129 -0
  251. package/viewer/index.html +151 -0
  252. package/lib/answerers/scripted/index.d.ts +0 -8
  253. package/lib/answerers/scripted/index.d.ts.map +0 -1
  254. package/lib/answerers/scripted/index.js +0 -163
  255. package/lib/answerers/scripted/index.js.map +0 -1
  256. package/src/answerers/scripted/index.ts +0 -186
@@ -4,6 +4,7 @@ import {
4
4
  createToolOutcomeAccumulator,
5
5
  emptyRunUsage,
6
6
  type GradingAssertionEntry,
7
+ type GradingReport,
7
8
  harnessUsage,
8
9
  isUsageEmpty,
9
10
  type LifecycleStage,
@@ -77,6 +78,7 @@ type CaseView = {
77
78
  assertionTotal: number;
78
79
  currentAssertion: string | null;
79
80
  assertions: GradingAssertionEntry[];
81
+ skippedAssertions: NonNullable<GradingReport["skippedAssertions"]>;
80
82
  usage: RunUsage;
81
83
  /** Per-API-call agent usage, in order — the split behind `usage.agent`. */
82
84
  agentTurns: TurnUsage[];
@@ -117,7 +119,10 @@ const statusMark = (status: CaseStatus, theme: Theme): string => {
117
119
 
118
120
  const assertionTally = (view: CaseView): string => {
119
121
  const passed = view.assertions.filter((a) => a.passed).length;
120
- return view.assertions.length === 0 ? "—" : `${passed}/${view.assertions.length}`;
122
+ const evaluated = view.assertions.length === 0 ? "—" : `${passed}/${view.assertions.length}`;
123
+ return view.skippedAssertions.length > 0
124
+ ? `${evaluated} · ${view.skippedAssertions.length} skipped`
125
+ : evaluated;
121
126
  };
122
127
 
123
128
  /**
@@ -233,6 +238,7 @@ export class ConsoleReporter implements SuiteObserver {
233
238
  assertionTotal: 0,
234
239
  currentAssertion: null,
235
240
  assertions: [],
241
+ skippedAssertions: [],
236
242
  usage: emptyRunUsage(),
237
243
  agentTurns: [],
238
244
  phaseDurationMs: null,
@@ -299,6 +305,7 @@ export class ConsoleReporter implements SuiteObserver {
299
305
  if (result.errorMessage) view.errorMessage = result.errorMessage;
300
306
  if (result.result) {
301
307
  view.assertions = result.result.grading.assertions;
308
+ view.skippedAssertions = result.result.grading.skippedAssertions ?? [];
302
309
  view.usage = result.result.metadata.usage;
303
310
  view.phaseDurationMs = result.result.metadata.phaseDurationMs ?? null;
304
311
  const tools = result.result.metadata.toolOutcomes;
@@ -315,6 +322,7 @@ export class ConsoleReporter implements SuiteObserver {
315
322
  // Assertions reported before the run failed to persist are not results
316
323
  // and must not inflate the closing tally of a case that errored.
317
324
  view.assertions = [];
325
+ view.skippedAssertions = [];
318
326
  }
319
327
 
320
328
  this.region.staticLine(this.finishedLine(view));
@@ -452,6 +460,11 @@ export class ConsoleReporter implements SuiteObserver {
452
460
  body: !entry.passed && entry.comment ? [this.theme.dim(entry.comment)] : [],
453
461
  });
454
462
  }
463
+ for (const skipped of view.skippedAssertions) {
464
+ nodes.push({
465
+ head: `${this.theme.dim("⊘")} ${skipped.name} ${this.theme.dim("skipped: judge disabled")}`,
466
+ });
467
+ }
455
468
 
456
469
  if (view.toolCalls > 0 || view.toolUnmatchedResults > 0 || view.toolMalformed > 0) {
457
470
  const parts = [
@@ -550,15 +563,17 @@ export class ConsoleReporter implements SuiteObserver {
550
563
  }
551
564
 
552
565
  const assertions = this.cases.flatMap((c) => c.assertions);
553
- if (assertions.length > 0) {
566
+ const skippedAssertions = this.cases.flatMap((c) => c.skippedAssertions);
567
+ if (assertions.length > 0 || skippedAssertions.length > 0) {
554
568
  const ok = assertions.filter((a) => a.passed).length;
555
569
  const bad = assertions.length - ok;
556
570
  const tally = formatTally(
557
571
  [
558
572
  [ok, this.theme.green("passed")],
559
573
  [bad, this.theme.red("failed")],
574
+ [skippedAssertions.length, this.theme.dim("skipped")],
560
575
  ],
561
- assertions.length,
576
+ assertions.length + skippedAssertions.length,
562
577
  );
563
578
  lines.push(` ${label("Assertions")}${tally}`);
564
579
  }
@@ -8,6 +8,7 @@ export type RunOptions = {
8
8
  listTags?: boolean;
9
9
  interactive?: boolean;
10
10
  claudeAnswerer?: boolean;
11
+ skipJudge?: boolean;
11
12
  runsRoot?: string;
12
13
  profile?: string;
13
14
  subjectRoot?: string;
@@ -0,0 +1,168 @@
1
+ import { resolve } from "node:path";
2
+ import { InvalidArgumentError } from "commander";
3
+ import {
4
+ formatExperimentRun,
5
+ formatExperimentRunsTable,
6
+ formatRunDetail,
7
+ formatRunsTable,
8
+ formatSuiteRunDetail,
9
+ formatSuiteRunsTable,
10
+ isExperimentRun,
11
+ listExperimentRuns,
12
+ listRuns,
13
+ listSuiteRuns,
14
+ loadExperimentRun,
15
+ loadLuxConfig,
16
+ loadRunDetail,
17
+ loadSuiteRun,
18
+ } from "../orchestrator/index.js";
19
+ import { openPreviewUrl, startPreviewServer } from "../previewer/server.js";
20
+ import { abortOnInterrupt, ensureDirectory, failWith } from "./command-runtime.js";
21
+
22
+ const SIGINT_EXIT_CODE = 130;
23
+
24
+ export type ViewOptions = {
25
+ runsRoot?: string;
26
+ case?: string;
27
+ suites?: boolean;
28
+ experiments?: boolean;
29
+ web?: boolean;
30
+ port?: number;
31
+ open?: boolean;
32
+ json?: boolean;
33
+ };
34
+
35
+ const previewKind = async (
36
+ initialPath: string | undefined,
37
+ options: ViewOptions,
38
+ ): Promise<"run" | "suite" | "experiment" | undefined> => {
39
+ if (initialPath) {
40
+ if (isExperimentRun(initialPath)) return "experiment";
41
+ if (await loadSuiteRun(initialPath)) return "suite";
42
+ return "run";
43
+ }
44
+ if (options.experiments) return "experiment";
45
+ if (options.suites) return "suite";
46
+ return undefined;
47
+ };
48
+
49
+ const webViewCommand = async (
50
+ runDirArg: string | undefined,
51
+ options: ViewOptions,
52
+ ): Promise<void> => {
53
+ if (options.json) {
54
+ throw new InvalidArgumentError("--web and --json are mutually exclusive");
55
+ }
56
+ const config = await loadLuxConfig(process.cwd());
57
+ const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
58
+ const initialPath = runDirArg ? resolve(runDirArg) : undefined;
59
+ if (initialPath && !(await ensureDirectory("view", initialPath))) return;
60
+ const initialKind = await previewKind(initialPath, options);
61
+ const controller = abortOnInterrupt();
62
+ const preview = await startPreviewServer({
63
+ runsRoot,
64
+ ...(options.port !== undefined ? { port: options.port } : {}),
65
+ ...(initialPath ? { initialPath } : {}),
66
+ ...(initialKind ? { initialKind } : {}),
67
+ ...(options.case ? { caseFilter: options.case } : {}),
68
+ signal: controller.signal,
69
+ });
70
+ process.stdout.write(`Lux Review: ${preview.url}\n`);
71
+ process.stdout.write(`Reading evidence from ${runsRoot}\n`);
72
+ if (options.open !== false) {
73
+ try {
74
+ await openPreviewUrl(preview.url);
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ process.stderr.write(`lux view: could not open a browser (${message})\n`);
78
+ }
79
+ }
80
+ await preview.closed;
81
+ if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
82
+ };
83
+
84
+ export const viewCommand = async (
85
+ runDirArg: string | undefined,
86
+ options: ViewOptions,
87
+ ): Promise<void> => {
88
+ if (options.web) {
89
+ await webViewCommand(runDirArg, options);
90
+ return;
91
+ }
92
+ if (options.port !== undefined || options.open === false) {
93
+ throw new InvalidArgumentError("--port and --no-open require --web");
94
+ }
95
+ if (runDirArg) {
96
+ const path = resolve(runDirArg);
97
+ if (!(await ensureDirectory("view", path))) return;
98
+ if (isExperimentRun(path)) {
99
+ const experiment = await loadExperimentRun(path);
100
+ if (!experiment) {
101
+ process.stderr.write(`lux view: invalid experiment at ${path}\n`);
102
+ failWith(1);
103
+ return;
104
+ }
105
+ process.stdout.write(
106
+ options.json
107
+ ? `${JSON.stringify({ experimentDir: path, ...experiment }, null, 2)}\n`
108
+ : `${formatExperimentRun(path, experiment)}\n`,
109
+ );
110
+ return;
111
+ }
112
+ const suite = await loadSuiteRun(path);
113
+ if (suite) {
114
+ process.stdout.write(
115
+ options.json
116
+ ? `${JSON.stringify({ suiteDir: path, ...suite }, null, 2)}\n`
117
+ : `${formatSuiteRunDetail(path, suite)}\n`,
118
+ );
119
+ return;
120
+ }
121
+ const detail = await loadRunDetail(path);
122
+ process.stdout.write(
123
+ options.json ? `${JSON.stringify(detail, null, 2)}\n` : `${formatRunDetail(detail)}\n`,
124
+ );
125
+ return;
126
+ }
127
+
128
+ const config = await loadLuxConfig(process.cwd());
129
+ const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
130
+ if (options.experiments) {
131
+ const experiments = await listExperimentRuns(runsRoot);
132
+ process.stdout.write(
133
+ options.json
134
+ ? `${JSON.stringify(experiments, null, 2)}\n`
135
+ : `experiments at ${runsRoot}:\n${formatExperimentRunsTable(experiments)}\n`,
136
+ );
137
+ return;
138
+ }
139
+ if (options.suites) {
140
+ const suites = await listSuiteRuns(runsRoot);
141
+ process.stdout.write(
142
+ options.json
143
+ ? `${JSON.stringify(suites, null, 2)}\n`
144
+ : `suite runs at ${runsRoot}:\n${formatSuiteRunsTable(suites)}\n`,
145
+ );
146
+ return;
147
+ }
148
+ const all = await listRuns(runsRoot);
149
+ const runs = options.case ? all.filter((run) => run.caseSlug === options.case) : all;
150
+ if (options.json) {
151
+ process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
152
+ return;
153
+ }
154
+ if (!options.case) {
155
+ process.stdout.write(`suite runs at ${runsRoot}:\n`);
156
+ process.stdout.write(`${formatSuiteRunsTable(await listSuiteRuns(runsRoot))}\n\n`);
157
+ }
158
+ process.stdout.write(`runs at ${runsRoot}:\n`);
159
+ process.stdout.write(`${formatRunsTable(runs)}\n`);
160
+ };
161
+
162
+ export const parsePreviewPort = (value: string): number => {
163
+ const port = Number(value);
164
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
165
+ throw new InvalidArgumentError("port must be an integer from 0 to 65535");
166
+ }
167
+ return port;
168
+ };
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import type { RunEvent } from "./events.js";
3
+
4
+ export const ActionCategorySchema = z.enum([
5
+ "query",
6
+ "mutation",
7
+ "durable-mutation",
8
+ "execution",
9
+ "interaction",
10
+ ]);
11
+ export type ActionCategory = z.infer<typeof ActionCategorySchema>;
12
+
13
+ export const ActionProgressPayloadSchema = z
14
+ .object({
15
+ name: z.string(),
16
+ category: ActionCategorySchema,
17
+ })
18
+ .strict();
19
+ export type ActionProgressPayload = z.infer<typeof ActionProgressPayloadSchema>;
20
+
21
+ export const ActionTimingSchema = z.object({
22
+ timeToFirstActionMs: z.exactOptional(z.number().nonnegative()),
23
+ timeToFirstMutationMs: z.exactOptional(z.number().nonnegative()),
24
+ timeToFirstDurableMutationMs: z.exactOptional(z.number().nonnegative()),
25
+ });
26
+ export type ActionTiming = z.infer<typeof ActionTimingSchema>;
27
+
28
+ export const actionTiming = (events: readonly RunEvent[]): ActionTiming => {
29
+ const startEvent = events.find(
30
+ (event) => event.type === "lifecycle" && event.stage === "start-driver",
31
+ );
32
+ const startedAt = startEvent?.type === "lifecycle" ? startEvent.ts : undefined;
33
+ if (startedAt === undefined) return {};
34
+
35
+ const actions = events.flatMap((event) => {
36
+ if (event.type !== "progress" || event.kind !== "action") return [];
37
+ const payload = ActionProgressPayloadSchema.safeParse(event.payload).data;
38
+ return payload ? [{ ...payload, ts: event.ts }] : [];
39
+ });
40
+ const elapsed = (category: "any" | "mutation" | "durable-mutation") => {
41
+ const timestamps = actions.flatMap(({ category: candidate, ts }) => {
42
+ if (category === "any") return [ts];
43
+ if (category === "mutation") {
44
+ return candidate === "mutation" || candidate === "durable-mutation" ? [ts] : [];
45
+ }
46
+ return candidate === category ? [ts] : [];
47
+ });
48
+ return timestamps.length > 0 ? Math.max(0, Math.min(...timestamps) - startedAt) : undefined;
49
+ };
50
+
51
+ const timeToFirstActionMs = elapsed("any");
52
+ const timeToFirstMutationMs = elapsed("mutation");
53
+ const timeToFirstDurableMutationMs = elapsed("durable-mutation");
54
+ return {
55
+ ...(timeToFirstActionMs === undefined ? {} : { timeToFirstActionMs }),
56
+ ...(timeToFirstMutationMs === undefined ? {} : { timeToFirstMutationMs }),
57
+ ...(timeToFirstDurableMutationMs === undefined ? {} : { timeToFirstDurableMutationMs }),
58
+ };
59
+ };
@@ -0,0 +1,236 @@
1
+ import { z } from "zod";
2
+ import { ActionProgressPayloadSchema } from "./action-events.js";
3
+ import { AgentCompletedPayloadSchema, AgentSpawnPayloadSchema } from "./agent-events.js";
4
+ import type { AgentAttribution, ReadyState, RunEvent } from "./events.js";
5
+ import { ToolCallPayloadSchema, ToolResultPayloadSchema } from "./tool-events.js";
6
+ import { TokenUsageSchema } from "./usage.js";
7
+
8
+ export const AgentActivitySchema = z.object({
9
+ agent: z.object({
10
+ id: z.string(),
11
+ role: z.enum(["main", "subagent"]),
12
+ parentId: z.exactOptional(z.string()),
13
+ spawnToolUseId: z.exactOptional(z.string()),
14
+ name: z.exactOptional(z.string()),
15
+ model: z.exactOptional(z.string()),
16
+ }),
17
+ startedAt: z.number(),
18
+ endedAt: z.number(),
19
+ durationMs: z.number().nonnegative(),
20
+ status: z.enum(["completed", "failed", "cancelled", "unknown"]),
21
+ eventCount: z.number().int().nonnegative(),
22
+ usage: TokenUsageSchema.extend({ total: z.number().int().nonnegative() }),
23
+ toolCalls: z.object({
24
+ total: z.number().int().nonnegative(),
25
+ succeeded: z.number().int().nonnegative(),
26
+ failed: z.number().int().nonnegative(),
27
+ unresolved: z.number().int().nonnegative(),
28
+ }),
29
+ actions: z.object({
30
+ total: z.number().int().nonnegative(),
31
+ mutations: z.number().int().nonnegative(),
32
+ durableMutations: z.number().int().nonnegative(),
33
+ timeToFirstActionMs: z.exactOptional(z.number().nonnegative()),
34
+ timeToFirstMutationMs: z.exactOptional(z.number().nonnegative()),
35
+ timeToFirstDurableMutationMs: z.exactOptional(z.number().nonnegative()),
36
+ }),
37
+ });
38
+ export type AgentActivity = z.infer<typeof AgentActivitySchema>;
39
+
40
+ const legacyAttribution = (
41
+ event: Extract<RunEvent, { type: "progress" }>,
42
+ ready: ReadyState,
43
+ ): AgentAttribution => {
44
+ const payload = event.payload;
45
+ const parentToolUseId =
46
+ typeof payload === "object" && payload !== null && "parentToolUseId" in payload
47
+ ? payload.parentToolUseId
48
+ : undefined;
49
+ const spawnToolUseId =
50
+ typeof parentToolUseId === "string" && parentToolUseId.length > 0 ? parentToolUseId : undefined;
51
+ return spawnToolUseId
52
+ ? {
53
+ id: `subagent:${spawnToolUseId}`,
54
+ role: "subagent" as const,
55
+ parentId: ready.agentId,
56
+ spawnToolUseId,
57
+ }
58
+ : { id: ready.agentId, role: "main" as const, ...(ready.model ? { model: ready.model } : {}) };
59
+ };
60
+
61
+ type MutableActivity = AgentActivity & { calls: Map<string, "pending" | "succeeded" | "failed"> };
62
+ type ProgressEvent = Extract<RunEvent, { type: "progress" }>;
63
+ type SpawnInfo = { ts: number; name?: string; model?: string };
64
+
65
+ const initialActivity = (agent: AgentAttribution, startedAt: number): MutableActivity => ({
66
+ agent,
67
+ startedAt,
68
+ endedAt: startedAt,
69
+ durationMs: 0,
70
+ status: "unknown",
71
+ eventCount: 0,
72
+ usage: { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 },
73
+ toolCalls: { total: 0, succeeded: 0, failed: 0, unresolved: 0 },
74
+ actions: { total: 0, mutations: 0, durableMutations: 0 },
75
+ calls: new Map(),
76
+ });
77
+
78
+ const collectSpawnInfo = (events: RunEvent[]): Map<string, SpawnInfo> => {
79
+ const spawns = new Map<string, SpawnInfo>();
80
+ for (const event of events) {
81
+ if (event.type !== "progress" || event.kind !== "tool-call") continue;
82
+ const call = ToolCallPayloadSchema.safeParse(event.payload).data;
83
+ if (!call) continue;
84
+ const input = z
85
+ .object({
86
+ name: z.string().min(1).optional(),
87
+ subagent_type: z.string().min(1).optional(),
88
+ agent: z.string().min(1).optional(),
89
+ role: z.string().min(1).optional(),
90
+ model: z.string().min(1).optional(),
91
+ })
92
+ .loose()
93
+ .safeParse(call.input).data;
94
+ const name = input?.name ?? input?.subagent_type ?? input?.agent ?? input?.role;
95
+ spawns.set(call.toolUseId, {
96
+ ts: event.ts,
97
+ ...(name ? { name } : {}),
98
+ ...(input?.model ? { model: input.model } : {}),
99
+ });
100
+ }
101
+ return spawns;
102
+ };
103
+
104
+ const applyLifecycle = (
105
+ event: ProgressEvent,
106
+ activities: Map<string, MutableActivity>,
107
+ ): boolean => {
108
+ if (event.kind === "agent-spawn") {
109
+ const spawn = AgentSpawnPayloadSchema.safeParse(event.payload).data;
110
+ if (spawn && !activities.has(spawn.agent.id)) {
111
+ activities.set(spawn.agent.id, initialActivity(spawn.agent, event.ts));
112
+ }
113
+ return true;
114
+ }
115
+ if (event.kind !== "agent-completed") return false;
116
+ const completed = AgentCompletedPayloadSchema.safeParse(event.payload).data;
117
+ const activity = completed ? activities.get(completed.agentId) : undefined;
118
+ if (activity && completed) {
119
+ activity.endedAt = Math.max(activity.endedAt, event.ts);
120
+ activity.durationMs = Math.max(0, activity.endedAt - activity.startedAt);
121
+ activity.status = completed.status;
122
+ }
123
+ return true;
124
+ };
125
+
126
+ const activityFor = (
127
+ event: ProgressEvent,
128
+ ready: ReadyState,
129
+ driverStartedAt: number,
130
+ spawns: Map<string, SpawnInfo>,
131
+ activities: Map<string, MutableActivity>,
132
+ ): MutableActivity => {
133
+ const agent = event.agent ?? legacyAttribution(event, ready);
134
+ const existing = activities.get(agent.id);
135
+ if (existing) return existing;
136
+ const spawn = agent.spawnToolUseId ? spawns.get(agent.spawnToolUseId) : undefined;
137
+ const name = agent.name ?? spawn?.name;
138
+ const model = agent.model ?? spawn?.model;
139
+ const attributedAgent = {
140
+ ...agent,
141
+ ...(name ? { name } : {}),
142
+ ...(model ? { model } : {}),
143
+ };
144
+ const startedAt =
145
+ agent.role === "subagent" && agent.spawnToolUseId
146
+ ? (spawn?.ts ?? event.ts)
147
+ : driverStartedAt || event.ts;
148
+ return activities.get(agent.id) ?? initialActivity(attributedAgent, startedAt);
149
+ };
150
+
151
+ const addAction = (event: ProgressEvent, activity: MutableActivity): void => {
152
+ const action = ActionProgressPayloadSchema.safeParse(event.payload).data;
153
+ if (!action) return;
154
+ const elapsed = Math.max(0, event.ts - activity.startedAt);
155
+ activity.actions.total++;
156
+ activity.actions.timeToFirstActionMs ??= elapsed;
157
+ if (action.category === "mutation" || action.category === "durable-mutation") {
158
+ activity.actions.mutations++;
159
+ activity.actions.timeToFirstMutationMs ??= elapsed;
160
+ }
161
+ if (action.category === "durable-mutation") {
162
+ activity.actions.durableMutations++;
163
+ activity.actions.timeToFirstDurableMutationMs ??= elapsed;
164
+ }
165
+ };
166
+
167
+ const addUsage = (event: ProgressEvent, activity: MutableActivity): void => {
168
+ const usage = TokenUsageSchema.safeParse(event.payload).data;
169
+ if (!usage) return;
170
+ activity.usage.input += usage.input;
171
+ activity.usage.output += usage.output;
172
+ activity.usage.cacheRead += usage.cacheRead;
173
+ activity.usage.cacheCreation += usage.cacheCreation;
174
+ activity.usage.total += usage.input + usage.output + usage.cacheRead + usage.cacheCreation;
175
+ };
176
+
177
+ const addWork = (event: ProgressEvent, activity: MutableActivity): void => {
178
+ activity.eventCount++;
179
+ activity.endedAt = Math.max(activity.endedAt, event.ts);
180
+ activity.durationMs = Math.max(0, activity.endedAt - activity.startedAt);
181
+ if (event.kind === "tool-call") {
182
+ const call = ToolCallPayloadSchema.safeParse(event.payload).data;
183
+ if (call && !activity.calls.has(call.toolUseId)) {
184
+ activity.calls.set(call.toolUseId, "pending");
185
+ activity.toolCalls.total++;
186
+ }
187
+ return;
188
+ }
189
+ if (event.kind === "tool-result") {
190
+ const result = ToolResultPayloadSchema.safeParse(event.payload).data;
191
+ if (result && activity.calls.has(result.toolUseId)) {
192
+ activity.calls.set(result.toolUseId, result.isError ? "failed" : "succeeded");
193
+ }
194
+ return;
195
+ }
196
+ if (event.kind === "action") addAction(event, activity);
197
+ if (event.kind === "usage") addUsage(event, activity);
198
+ };
199
+
200
+ export const summarizeAgentActivity = (events: RunEvent[], ready: ReadyState): AgentActivity[] => {
201
+ const driverStart = events.find(
202
+ (event): event is Extract<RunEvent, { type: "lifecycle" }> =>
203
+ event.type === "lifecycle" && event.stage === "start-driver",
204
+ );
205
+ const driverStartedAt = driverStart?.ts ?? 0;
206
+ const spawns = collectSpawnInfo(events);
207
+ const activities = new Map<string, MutableActivity>();
208
+ for (const event of events) {
209
+ if (event.type !== "progress") continue;
210
+ if (applyLifecycle(event, activities)) continue;
211
+ const activity = activityFor(event, ready, driverStartedAt, spawns, activities);
212
+ addWork(event, activity);
213
+ activities.set(activity.agent.id, activity);
214
+ }
215
+
216
+ if (!activities.has(ready.agentId)) {
217
+ activities.set(
218
+ ready.agentId,
219
+ initialActivity(
220
+ {
221
+ id: ready.agentId,
222
+ role: "main",
223
+ ...(ready.model ? { model: ready.model } : {}),
224
+ },
225
+ driverStartedAt,
226
+ ),
227
+ );
228
+ }
229
+ return [...activities.values()]
230
+ .map(({ calls, ...activity }) => {
231
+ for (const outcome of calls.values())
232
+ activity.toolCalls[outcome === "pending" ? "unresolved" : outcome]++;
233
+ return AgentActivitySchema.parse(activity);
234
+ })
235
+ .sort((a, b) => a.startedAt - b.startedAt || a.agent.id.localeCompare(b.agent.id));
236
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { AgentAttributionSchema } from "./events.js";
3
+
4
+ export const AGENT_SPAWN_KIND = "agent-spawn" as const;
5
+ export const AGENT_COMPLETED_KIND = "agent-completed" as const;
6
+
7
+ export const AgentSpawnPayloadSchema = z.object({
8
+ agent: AgentAttributionSchema.extend({ role: z.literal("subagent") }),
9
+ prompt: z.exactOptional(z.string()),
10
+ });
11
+ export type AgentSpawnPayload = z.infer<typeof AgentSpawnPayloadSchema>;
12
+
13
+ export const AgentCompletedPayloadSchema = z.object({
14
+ agentId: z.string().min(1),
15
+ status: z.enum(["completed", "failed", "cancelled", "unknown"]),
16
+ });
17
+ export type AgentCompletedPayload = z.infer<typeof AgentCompletedPayloadSchema>;
@@ -7,12 +7,15 @@ import type { Run } from "./run.js";
7
7
  const DEFAULT_ARTIFACT_READ_BYTES = 5 * 1024 * 1024;
8
8
  const MAX_ARTIFACT_READ_BYTES = 10 * 1024 * 1024;
9
9
 
10
+ export type RunArtifactIssueReason = "absent" | "unreadable";
11
+
10
12
  export type RunArtifactInspection = {
11
13
  target: string;
12
14
  root: string;
13
15
  artifact?: Artifact;
14
16
  stats?: Stats;
15
17
  issue?: string;
18
+ reason?: RunArtifactIssueReason;
16
19
  };
17
20
 
18
21
  type RunArtifactEvidence = {
@@ -82,6 +85,7 @@ export const inspectRunArtifact = async (
82
85
  target: authoredPath,
83
86
  root: runArtifactsRoot(run),
84
87
  issue: `${authoredPath} contains a null byte`,
88
+ reason: "unreadable",
85
89
  };
86
90
  }
87
91
  const matching = run.artifacts.filter((candidate) => candidate.path === authoredPath);
@@ -91,6 +95,7 @@ export const inspectRunArtifact = async (
91
95
  target: resolve(root, authoredPath),
92
96
  root,
93
97
  issue: `${authoredPath} was not captured in the frozen artifact index`,
98
+ reason: "absent",
94
99
  };
95
100
  }
96
101
  if (matching.some((candidate) => candidate.root !== undefined && !isAbsolute(candidate.root))) {
@@ -98,6 +103,7 @@ export const inspectRunArtifact = async (
98
103
  target: authoredPath,
99
104
  root: defaultArtifactsRoot(run),
100
105
  issue: `${authoredPath} declares a non-absolute artifact root`,
106
+ reason: "unreadable",
101
107
  };
102
108
  }
103
109
  const artifact = matching[0];
@@ -108,6 +114,7 @@ export const inspectRunArtifact = async (
108
114
  target: authoredPath,
109
115
  root: defaultArtifactsRoot(run),
110
116
  issue: `${authoredPath} is ambiguous across multiple artifact roots`,
117
+ reason: "unreadable",
111
118
  };
112
119
  }
113
120
  // A non-default root is trusted only for an exact artifact-index entry.
@@ -116,7 +123,12 @@ export const inspectRunArtifact = async (
116
123
  const root = matchingRoots[0] ?? defaultArtifactsRoot(run);
117
124
  const target = resolve(root, authoredPath);
118
125
  if (pathEscapesRoot(root, target)) {
119
- return { target, root, issue: `${authoredPath} escapes the captured artifact root` };
126
+ return {
127
+ target,
128
+ root,
129
+ issue: `${authoredPath} escapes the captured artifact root`,
130
+ reason: "unreadable",
131
+ };
120
132
  }
121
133
 
122
134
  const path = relative(root, target);
@@ -134,7 +146,13 @@ export const inspectRunArtifact = async (
134
146
  } catch (error) {
135
147
  const code = (error as NodeJS.ErrnoException).code;
136
148
  if (code === "ENOENT" || code === "ENOTDIR") {
137
- return { target, root, ...identified, issue: `${authoredPath} not found` };
149
+ return {
150
+ target,
151
+ root,
152
+ ...identified,
153
+ issue: `${authoredPath} not found`,
154
+ reason: "absent",
155
+ };
138
156
  }
139
157
  throw error;
140
158
  }
@@ -144,16 +162,17 @@ export const inspectRunArtifact = async (
144
162
  root,
145
163
  ...identified,
146
164
  issue: `${authoredPath} resolves through a symlink`,
165
+ reason: "unreadable",
147
166
  };
148
167
  }
149
168
  if (index < components.length - 1 && !stats.isDirectory()) {
150
- return { target, root, ...identified, issue: `${authoredPath} not found` };
169
+ return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
151
170
  }
152
171
  if (index === components.length - 1) {
153
172
  return { target, root, ...identified, stats };
154
173
  }
155
174
  }
156
- return { target, root, ...identified, issue: `${authoredPath} not found` };
175
+ return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
157
176
  };
158
177
 
159
178
  export type ReadRunArtifactTextOptions = {
@@ -247,3 +266,27 @@ export const readRunArtifactText = async (
247
266
  ): Promise<string> => {
248
267
  return (await readRunArtifactBytes(run, authoredPath, options)).toString("utf8");
249
268
  };
269
+
270
+ /**
271
+ * Read optional artifact evidence. Returns null when the artifact was never
272
+ * captured, and still throws when one exists but is unsafe to consume, so a
273
+ * traversal or symlink violation is never mistaken for missing evidence.
274
+ */
275
+ export const readRunArtifactTextIfPresent = async (
276
+ run: RunArtifactEvidence,
277
+ authoredPath: string,
278
+ options: ReadRunArtifactTextOptions = {},
279
+ ): Promise<string | null> => {
280
+ const inspected = await inspectRunArtifact(run, authoredPath);
281
+ if (inspected.reason === "absent") return null;
282
+ if (!inspected.stats?.isFile()) {
283
+ throw new Error(
284
+ inspected.issue ??
285
+ (inspected.stats ? `${authoredPath} is not a file` : `${authoredPath} not found`),
286
+ );
287
+ }
288
+ const maxBytes = options.maxBytes ?? DEFAULT_ARTIFACT_READ_BYTES;
289
+ return (
290
+ await readInspectedFileBytes(inspected.target, inspected.stats, maxBytes, authoredPath)
291
+ ).toString("utf8");
292
+ };