@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.
- package/lib/answerers/persona/index.d.ts +2 -1
- package/lib/answerers/persona/index.d.ts.map +1 -1
- package/lib/answerers/persona/index.js +19 -5
- package/lib/answerers/persona/index.js.map +1 -1
- package/lib/answerers/terminal/index.js +1 -1
- package/lib/answerers/terminal/index.js.map +1 -1
- package/lib/assertions/authoring.d.ts +14 -0
- package/lib/assertions/authoring.d.ts.map +1 -0
- package/lib/assertions/authoring.js +3 -0
- package/lib/assertions/authoring.js.map +1 -0
- package/lib/assertions/core/helpers.d.ts +2 -7
- package/lib/assertions/core/helpers.d.ts.map +1 -1
- package/lib/assertions/core/helpers.js +2 -6
- package/lib/assertions/core/helpers.js.map +1 -1
- package/lib/assertions/core/index.d.ts.map +1 -1
- package/lib/assertions/core/index.js +2 -1
- package/lib/assertions/core/index.js.map +1 -1
- package/lib/assertions/core/resource-checks.d.ts +19 -0
- package/lib/assertions/core/resource-checks.d.ts.map +1 -1
- package/lib/assertions/core/resource-checks.js +32 -0
- package/lib/assertions/core/resource-checks.js.map +1 -1
- package/lib/authoring.d.ts +1 -0
- package/lib/authoring.d.ts.map +1 -1
- package/lib/authoring.js +1 -0
- package/lib/authoring.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +12 -67
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/render/reporter.d.ts.map +1 -1
- package/lib/cli/render/reporter.js +16 -3
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/cli/run-options.d.ts +1 -0
- package/lib/cli/run-options.d.ts.map +1 -1
- package/lib/cli/run-options.js.map +1 -1
- package/lib/cli/view.d.ts +13 -0
- package/lib/cli/view.d.ts.map +1 -0
- package/lib/cli/view.js +126 -0
- package/lib/cli/view.js.map +1 -0
- package/lib/core/action-events.d.ts +29 -0
- package/lib/core/action-events.d.ts.map +1 -0
- package/lib/core/action-events.js +51 -0
- package/lib/core/action-events.js.map +1 -0
- package/lib/core/agent-activity.d.ts +49 -0
- package/lib/core/agent-activity.d.ts.map +1 -0
- package/lib/core/agent-activity.js +206 -0
- package/lib/core/agent-activity.js.map +1 -0
- package/lib/core/agent-events.d.ts +26 -0
- package/lib/core/agent-events.d.ts.map +1 -0
- package/lib/core/agent-events.js +13 -0
- package/lib/core/agent-events.js.map +1 -0
- package/lib/core/artifact-evidence.d.ts +8 -0
- package/lib/core/artifact-evidence.d.ts.map +1 -1
- package/lib/core/artifact-evidence.js +36 -4
- package/lib/core/artifact-evidence.js.map +1 -1
- package/lib/core/assertion.d.ts +19 -9
- package/lib/core/assertion.d.ts.map +1 -1
- package/lib/core/assertion.js +26 -0
- package/lib/core/assertion.js.map +1 -1
- package/lib/core/case-authoring-state.d.ts +12 -0
- package/lib/core/case-authoring-state.d.ts.map +1 -0
- package/lib/core/case-authoring-state.js +43 -0
- package/lib/core/case-authoring-state.js.map +1 -0
- package/lib/core/case.d.ts +29 -1
- package/lib/core/case.d.ts.map +1 -1
- package/lib/core/case.js +14 -6
- package/lib/core/case.js.map +1 -1
- package/lib/core/driver-metrics.d.ts +98 -0
- package/lib/core/driver-metrics.d.ts.map +1 -0
- package/lib/core/driver-metrics.js +36 -0
- package/lib/core/driver-metrics.js.map +1 -0
- package/lib/core/driver.d.ts +3 -0
- package/lib/core/driver.d.ts.map +1 -1
- package/lib/core/driver.js.map +1 -1
- package/lib/core/events.d.ts +36 -0
- package/lib/core/events.d.ts.map +1 -1
- package/lib/core/events.js +12 -2
- package/lib/core/events.js.map +1 -1
- package/lib/core/experiment.d.ts +19 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +2 -0
- package/lib/core/experiment.js.map +1 -1
- package/lib/core/index.d.ts +4 -0
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +4 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/lifecycle-fixtures.d.ts +3 -1
- package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
- package/lib/core/lifecycle-fixtures.js +4 -2
- package/lib/core/lifecycle-fixtures.js.map +1 -1
- package/lib/core/lifecycle-hooks.d.ts +9 -0
- package/lib/core/lifecycle-hooks.d.ts.map +1 -1
- package/lib/core/lifecycle-hooks.js +8 -2
- package/lib/core/lifecycle-hooks.js.map +1 -1
- package/lib/core/run.d.ts +150 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/run.js +9 -0
- package/lib/core/run.js.map +1 -1
- package/lib/core/wire.d.ts +12 -0
- package/lib/core/wire.d.ts.map +1 -1
- package/lib/core/wire.js +5 -1
- package/lib/core/wire.js.map +1 -1
- package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
- package/lib/drivers/claude-code/parse-events.js +73 -8
- package/lib/drivers/claude-code/parse-events.js.map +1 -1
- package/lib/drivers/codex/app-events.d.ts +9 -2
- package/lib/drivers/codex/app-events.d.ts.map +1 -1
- package/lib/drivers/codex/app-events.js +93 -12
- package/lib/drivers/codex/app-events.js.map +1 -1
- package/lib/drivers/codex/index.d.ts.map +1 -1
- package/lib/drivers/codex/index.js +10 -14
- package/lib/drivers/codex/index.js.map +1 -1
- package/lib/drivers/subprocess/index.d.ts.map +1 -1
- package/lib/drivers/subprocess/index.js +6 -0
- package/lib/drivers/subprocess/index.js.map +1 -1
- package/lib/index.d.ts +10 -6
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +10 -6
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/annotation-store.d.ts.map +1 -1
- package/lib/orchestrator/annotation-store.js +6 -2
- package/lib/orchestrator/annotation-store.js.map +1 -1
- package/lib/orchestrator/case-extensions.d.ts +6 -0
- package/lib/orchestrator/case-extensions.d.ts.map +1 -0
- package/lib/orchestrator/case-extensions.js +28 -0
- package/lib/orchestrator/case-extensions.js.map +1 -0
- package/lib/orchestrator/compare.d.ts +13 -1
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +12 -0
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +0 -2
- package/lib/orchestrator/config.js.map +1 -1
- package/lib/orchestrator/doctor.d.ts.map +1 -1
- package/lib/orchestrator/doctor.js +5 -3
- package/lib/orchestrator/doctor.js.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +19 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +17 -0
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +19 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +38 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/grader-experiment.d.ts +12 -0
- package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
- package/lib/orchestrator/grading.d.ts +2 -1
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +16 -4
- package/lib/orchestrator/grading.js.map +1 -1
- package/lib/orchestrator/index.d.ts +1 -1
- package/lib/orchestrator/index.d.ts.map +1 -1
- package/lib/orchestrator/index.js +1 -1
- package/lib/orchestrator/index.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts +7 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +50 -6
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/loader.d.ts.map +1 -1
- package/lib/orchestrator/loader.js +2 -0
- package/lib/orchestrator/loader.js.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts +3 -0
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +42 -14
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/lib/orchestrator/owned-lock.d.ts.map +1 -1
- package/lib/orchestrator/owned-lock.js +52 -38
- package/lib/orchestrator/owned-lock.js.map +1 -1
- package/lib/orchestrator/regrade.d.ts +6 -0
- package/lib/orchestrator/regrade.d.ts.map +1 -1
- package/lib/orchestrator/regrade.js +8 -4
- package/lib/orchestrator/regrade.js.map +1 -1
- package/lib/orchestrator/run-execution.d.ts +2 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +20 -5
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts +34 -0
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +68 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +45 -7
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/artifacts.d.ts +25 -0
- package/lib/previewer/artifacts.d.ts.map +1 -0
- package/lib/previewer/artifacts.js +180 -0
- package/lib/previewer/artifacts.js.map +1 -0
- package/lib/previewer/server.d.ts +105 -0
- package/lib/previewer/server.d.ts.map +1 -0
- package/lib/previewer/server.js +542 -0
- package/lib/previewer/server.js.map +1 -0
- package/package.json +2 -2
- package/skills/lux/SKILL.md +7 -3
- package/skills/lux/references/cli.md +25 -2
- package/skills/lux/references/eval-authoring.md +158 -4
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/persona/index.ts +20 -4
- package/src/answerers/terminal/index.ts +1 -1
- package/src/assertions/authoring.ts +81 -0
- package/src/assertions/core/helpers.ts +3 -6
- package/src/assertions/core/index.ts +2 -0
- package/src/assertions/core/resource-checks.ts +35 -0
- package/src/authoring.ts +1 -0
- package/src/cli/program.ts +14 -93
- package/src/cli/render/reporter.ts +18 -3
- package/src/cli/run-options.ts +1 -0
- package/src/cli/view.ts +168 -0
- package/src/core/action-events.ts +59 -0
- package/src/core/agent-activity.ts +236 -0
- package/src/core/agent-events.ts +17 -0
- package/src/core/artifact-evidence.ts +47 -4
- package/src/core/assertion.ts +41 -22
- package/src/core/case-authoring-state.ts +65 -0
- package/src/core/case.ts +57 -6
- package/src/core/driver-metrics.ts +51 -0
- package/src/core/driver.ts +3 -0
- package/src/core/events.ts +14 -2
- package/src/core/experiment.ts +2 -0
- package/src/core/index.ts +4 -0
- package/src/core/lifecycle-fixtures.ts +7 -2
- package/src/core/lifecycle-hooks.ts +16 -2
- package/src/core/run.ts +9 -0
- package/src/core/wire.ts +5 -1
- package/src/drivers/claude-code/parse-events.ts +98 -12
- package/src/drivers/codex/app-events.ts +116 -16
- package/src/drivers/codex/index.ts +16 -14
- package/src/drivers/subprocess/index.ts +6 -0
- package/src/index.ts +50 -6
- package/src/orchestrator/annotation-store.ts +5 -1
- package/src/orchestrator/case-extensions.ts +44 -0
- package/src/orchestrator/compare.ts +18 -0
- package/src/orchestrator/config.ts +0 -2
- package/src/orchestrator/doctor.ts +5 -3
- package/src/orchestrator/experiment-evaluation.ts +23 -0
- package/src/orchestrator/grading.ts +21 -2
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/list-runs.ts +66 -5
- package/src/orchestrator/loader.ts +2 -0
- package/src/orchestrator/orchestrator.ts +86 -40
- package/src/orchestrator/owned-lock.ts +54 -34
- package/src/orchestrator/regrade.ts +8 -7
- package/src/orchestrator/run-execution.ts +24 -3
- package/src/orchestrator/suite-summary.ts +101 -8
- package/src/previewer/artifacts.ts +221 -0
- package/src/previewer/server.ts +765 -0
- package/viewer/app.css +2075 -0
- package/viewer/app.js +606 -0
- package/viewer/artifact-view.js +48 -0
- package/viewer/charts.js +86 -0
- package/viewer/detail.js +1214 -0
- package/viewer/format.js +129 -0
- package/viewer/index.html +151 -0
- package/lib/answerers/scripted/index.d.ts +0 -8
- package/lib/answerers/scripted/index.d.ts.map +0 -1
- package/lib/answerers/scripted/index.js +0 -163
- package/lib/answerers/scripted/index.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/cli/run-options.ts
CHANGED
package/src/cli/view.ts
ADDED
|
@@ -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 {
|
|
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 {
|
|
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
|
+
};
|