reeval 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/THIRD_PARTY_NOTICES.md +2160 -0
- package/assets/package.json +9 -0
- package/assets/pnpm-lock.yaml +1336 -0
- package/dist/agent-DVHkx8aT.js +532 -0
- package/dist/agent-DVHkx8aT.js.map +1 -0
- package/dist/analysis-CxSCt1YY.js +16 -0
- package/dist/analysis-CxSCt1YY.js.map +1 -0
- package/dist/assemble-B-nmz8oH.js +695 -0
- package/dist/assemble-B-nmz8oH.js.map +1 -0
- package/dist/assemble-CmRVDZ2Q.js +2 -0
- package/dist/attempt-BdRQ9jKX.js +176 -0
- package/dist/attempt-BdRQ9jKX.js.map +1 -0
- package/dist/bridge/index.mjs +1911 -0
- package/dist/bridge/package.json +13 -0
- package/dist/bridge/pnpm-lock.yaml +1041 -0
- package/dist/bridge/pnpm-workspace.yaml +2 -0
- package/dist/budget-DBUrH4jX.js +8 -0
- package/dist/budget-DBUrH4jX.js.map +1 -0
- package/dist/cli--CQ3Iy9q.js +1865 -0
- package/dist/cli--CQ3Iy9q.js.map +1 -0
- package/dist/cli-spec-DKAu09Gi.js +478 -0
- package/dist/cli-spec-DKAu09Gi.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -0
- package/dist/colors-QV9dFVhN.js +34 -0
- package/dist/colors-QV9dFVhN.js.map +1 -0
- package/dist/command-BSJzHMtP.js +280 -0
- package/dist/command-BSJzHMtP.js.map +1 -0
- package/dist/command-CEm8PZ1N.js +1043 -0
- package/dist/command-CEm8PZ1N.js.map +1 -0
- package/dist/command-D-jnQWVG.js +673 -0
- package/dist/command-D-jnQWVG.js.map +1 -0
- package/dist/command-DQAvbG7d.js +432 -0
- package/dist/command-DQAvbG7d.js.map +1 -0
- package/dist/commands-A4udfyKZ.js +2 -0
- package/dist/commands-CF8mOfiu.js +207 -0
- package/dist/commands-CF8mOfiu.js.map +1 -0
- package/dist/config-Cuj0kVYj.js +415 -0
- package/dist/config-Cuj0kVYj.js.map +1 -0
- package/dist/context-loi9OCZv.js +106 -0
- package/dist/context-loi9OCZv.js.map +1 -0
- package/dist/core-tLN-z1Be.js +96 -0
- package/dist/core-tLN-z1Be.js.map +1 -0
- package/dist/db-Cjj2ZsfQ.js +2 -0
- package/dist/db-DxDzMhtB.js +827 -0
- package/dist/db-DxDzMhtB.js.map +1 -0
- package/dist/debug-D24NOQH1.js +89 -0
- package/dist/debug-D24NOQH1.js.map +1 -0
- package/dist/debug-SEmW4w8f.js +2 -0
- package/dist/dist-BZXYHN2X.js +1673 -0
- package/dist/dist-BZXYHN2X.js.map +1 -0
- package/dist/docker-DQDZWXF9.js +20 -0
- package/dist/docker-DQDZWXF9.js.map +1 -0
- package/dist/docker-DnWAVdEm.js +2 -0
- package/dist/docker-oSbCExHL.js +153 -0
- package/dist/docker-oSbCExHL.js.map +1 -0
- package/dist/doctor-CxlH1pDM.js +525 -0
- package/dist/doctor-CxlH1pDM.js.map +1 -0
- package/dist/egress-73SQfKdx.js +619 -0
- package/dist/egress-73SQfKdx.js.map +1 -0
- package/dist/env-BQqj93gu.js +21 -0
- package/dist/env-BQqj93gu.js.map +1 -0
- package/dist/environment-Mz7wvrtD.js +552 -0
- package/dist/environment-Mz7wvrtD.js.map +1 -0
- package/dist/errors-C9w_ud7g.js +160 -0
- package/dist/errors-C9w_ud7g.js.map +1 -0
- package/dist/errors-CSxfJ4qu.js +11 -0
- package/dist/errors-CSxfJ4qu.js.map +1 -0
- package/dist/files--6CEUXm7.js +2 -0
- package/dist/files-BLlrhU4-.js +62 -0
- package/dist/files-BLlrhU4-.js.map +1 -0
- package/dist/files-Bd25OAQH.js +2 -0
- package/dist/files-BvHCFWgD.js +2 -0
- package/dist/files-D2-qn_Dl.js +275 -0
- package/dist/files-D2-qn_Dl.js.map +1 -0
- package/dist/files-bF2fPuT9.js +99 -0
- package/dist/files-bF2fPuT9.js.map +1 -0
- package/dist/git-D6hMeieS.js +44 -0
- package/dist/git-D6hMeieS.js.map +1 -0
- package/dist/harness-ls5aX1UO.js +131 -0
- package/dist/harness-ls5aX1UO.js.map +1 -0
- package/dist/harness.d.ts +74 -0
- package/dist/harness.js +2 -0
- package/dist/header-DzFDoMCI.js +78 -0
- package/dist/header-DzFDoMCI.js.map +1 -0
- package/dist/ids-Bp6ut7ZD.js +49 -0
- package/dist/ids-Bp6ut7ZD.js.map +1 -0
- package/dist/images-Cc1-coFc.js +286 -0
- package/dist/images-Cc1-coFc.js.map +1 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +1959 -0
- package/dist/index.js.map +1 -0
- package/dist/init-CFljS8BJ.js +344 -0
- package/dist/init-CFljS8BJ.js.map +1 -0
- package/dist/init-z2ZPn9Tj.js +2 -0
- package/dist/invocations-rXktgSAn.js +157 -0
- package/dist/invocations-rXktgSAn.js.map +1 -0
- package/dist/jiti-BOLXpKyG.js +46 -0
- package/dist/jiti-BOLXpKyG.js.map +1 -0
- package/dist/judge-D8Uf4dA9.js +146 -0
- package/dist/judge-D8Uf4dA9.js.map +1 -0
- package/dist/judge.d.ts +21 -0
- package/dist/judge.js +19 -0
- package/dist/judge.js.map +1 -0
- package/dist/live-Bs3LReMV.js +98 -0
- package/dist/live-Bs3LReMV.js.map +1 -0
- package/dist/matchers-DyLvm7EG.js +374 -0
- package/dist/matchers-DyLvm7EG.js.map +1 -0
- package/dist/model-BdO79gWz.js +44 -0
- package/dist/model-BdO79gWz.js.map +1 -0
- package/dist/model-CDL98xue.js +725 -0
- package/dist/model-CDL98xue.js.map +1 -0
- package/dist/model-v-0_Ik7S.js +2 -0
- package/dist/node-kG64XxdE.js +506 -0
- package/dist/node-kG64XxdE.js.map +1 -0
- package/dist/opencode-providers-DZuD_oYt.js +3149 -0
- package/dist/opencode-providers-DZuD_oYt.js.map +1 -0
- package/dist/options-CAQhu--C.js +452 -0
- package/dist/options-CAQhu--C.js.map +1 -0
- package/dist/patch-BkKje4cp.js +69 -0
- package/dist/patch-BkKje4cp.js.map +1 -0
- package/dist/paths-CBvm6pap.js +72 -0
- package/dist/paths-CBvm6pap.js.map +1 -0
- package/dist/plan-gwk6dJ8c.js +237 -0
- package/dist/plan-gwk6dJ8c.js.map +1 -0
- package/dist/preflight-BtubXKQ8.js +2 -0
- package/dist/preflight-BypZMNBG.js +185 -0
- package/dist/preflight-BypZMNBG.js.map +1 -0
- package/dist/prepare-Cwv0bHs_.js +3 -0
- package/dist/prepare-D6Uzcubb.js +213 -0
- package/dist/prepare-D6Uzcubb.js.map +1 -0
- package/dist/provenance-Dfb8YY5Y.js +129 -0
- package/dist/provenance-Dfb8YY5Y.js.map +1 -0
- package/dist/provider-BDbNOQhQ.js +2 -0
- package/dist/provider-CmAYTQQs.js +14 -0
- package/dist/provider-CmAYTQQs.js.map +1 -0
- package/dist/providers-2p3ylMHI.js +107 -0
- package/dist/providers-2p3ylMHI.js.map +1 -0
- package/dist/providers-BUOxitcT.js +2 -0
- package/dist/query-CFATHZmV.js +91 -0
- package/dist/query-CFATHZmV.js.map +1 -0
- package/dist/query-DR4m6CXf.js +2 -0
- package/dist/record-DQ8wzyco.js +99 -0
- package/dist/record-DQ8wzyco.js.map +1 -0
- package/dist/redact-C4fL_bZP.js +167 -0
- package/dist/redact-C4fL_bZP.js.map +1 -0
- package/dist/results-CeXb8Wde.js +37 -0
- package/dist/results-CeXb8Wde.js.map +1 -0
- package/dist/root-CFDjpWuc.js +110 -0
- package/dist/root-CFDjpWuc.js.map +1 -0
- package/dist/sandbox-By2gkdkC.js +133 -0
- package/dist/sandbox-By2gkdkC.js.map +1 -0
- package/dist/sandbox.d.ts +41 -0
- package/dist/sandbox.js +2 -0
- package/dist/server-Bi61uiY0.js +350 -0
- package/dist/server-Bi61uiY0.js.map +1 -0
- package/dist/server-CsSBL8NC.js +2 -0
- package/dist/source-BXzrmof5.js +2 -0
- package/dist/source-Bsfa3LZH.js +40 -0
- package/dist/source-Bsfa3LZH.js.map +1 -0
- package/dist/src-55j1zNHH.js +3 -0
- package/dist/src-AEwxd1UP.js +602 -0
- package/dist/src-AEwxd1UP.js.map +1 -0
- package/dist/src-BDwSuyIo.js +3 -0
- package/dist/src-BQyMiWri.js +490 -0
- package/dist/src-BQyMiWri.js.map +1 -0
- package/dist/src-C-LHyTSV.js +3 -0
- package/dist/src-Cmp-7YrD.js +1136 -0
- package/dist/src-Cmp-7YrD.js.map +1 -0
- package/dist/src-D0RdqpNb.js +252 -0
- package/dist/src-D0RdqpNb.js.map +1 -0
- package/dist/src-D8KYzxBX.js +793 -0
- package/dist/src-D8KYzxBX.js.map +1 -0
- package/dist/src-DEgff9Z7.js +4 -0
- package/dist/src-DXx_29pq.js +567 -0
- package/dist/src-DXx_29pq.js.map +1 -0
- package/dist/src-DwwiVLqv.js +2937 -0
- package/dist/src-DwwiVLqv.js.map +1 -0
- package/dist/src-N7E_mfcZ.js +2 -0
- package/dist/src-W9g-y5X5.js +3 -0
- package/dist/src-daS0wKH2.js +3 -0
- package/dist/state-Cyw3QTsN.js +2 -0
- package/dist/state-DZ7eqb1F.js +167 -0
- package/dist/state-DZ7eqb1F.js.map +1 -0
- package/dist/stats-BNAHlNMD.js +114 -0
- package/dist/stats-BNAHlNMD.js.map +1 -0
- package/dist/studio-app/assets/index-D6XyfFSH.js +58 -0
- package/dist/studio-app/assets/index-xwzWdcE_.css +2 -0
- package/dist/studio-app/favicon.svg +1 -0
- package/dist/studio-app/index.html +15 -0
- package/dist/summary-D7Ttky0z.js +166 -0
- package/dist/summary-D7Ttky0z.js.map +1 -0
- package/dist/text-gEcKjAyp.js +29 -0
- package/dist/text-gEcKjAyp.js.map +1 -0
- package/dist/types-C5vI3g1C.js +9 -0
- package/dist/types-C5vI3g1C.js.map +1 -0
- package/dist/types-DLec-6Cx.d.ts +1535 -0
- package/dist/usage-DsYUqIUM.js +24 -0
- package/dist/usage-DsYUqIUM.js.map +1 -0
- package/dist/view/index.d.ts +645 -0
- package/dist/view/index.js +8 -0
- package/dist/views-BrHOv6wg.js +220 -0
- package/dist/views-BrHOv6wg.js.map +1 -0
- package/dist/views-CwCWLWqo.js +3 -0
- package/dist/worker-entry-DnYkANPG.js +506 -0
- package/dist/worker-entry-DnYkANPG.js.map +1 -0
- package/dist/workspace-C2S0Z6gl.js +327 -0
- package/dist/workspace-C2S0Z6gl.js.map +1 -0
- package/dist/workspace-CyoXG6CY.js +2 -0
- package/docker/Dockerfile +19 -0
- package/package.json +79 -0
|
@@ -0,0 +1,1865 @@
|
|
|
1
|
+
import { t as UsageError } from "./errors-CSxfJ4qu.js";
|
|
2
|
+
import { x as parseDuration, y as workspaceLabel } from "./environment-Mz7wvrtD.js";
|
|
3
|
+
import { n as loadEnvFiles, t as inCI } from "./env-BQqj93gu.js";
|
|
4
|
+
import { a as registerSecret, i as redactSecrets } from "./redact-C4fL_bZP.js";
|
|
5
|
+
import { o as PACKAGE_VERSION } from "./root-CFDjpWuc.js";
|
|
6
|
+
import { a as fromCwd, i as evalSlug, n as attemptKey, o as newRunId } from "./ids-Bp6ut7ZD.js";
|
|
7
|
+
import { I as LABEL, X as imageInfo } from "./src-DwwiVLqv.js";
|
|
8
|
+
import { s as harnessId, u as sandboxId } from "./options-CAQhu--C.js";
|
|
9
|
+
import { c as hostCapacity, d as missingCredentials, f as resolveConcurrency, i as configPathFor, l as loadConfig, p as selectProfile, r as configLabel, s as findConfig, u as malformedCredentials } from "./config-Cuj0kVYj.js";
|
|
10
|
+
import { s as debugTime } from "./debug-D24NOQH1.js";
|
|
11
|
+
import { i as sandboxWarningLine, n as clip, r as formatDuration } from "./text-gEcKjAyp.js";
|
|
12
|
+
import { t as PrepareError } from "./provider-CmAYTQQs.js";
|
|
13
|
+
import { N as effortLabel, O as cellContrast, b as runOutcome, h as hasUngatedCells, p as formatUsage, v as isUngatedMiss } from "./model-CDL98xue.js";
|
|
14
|
+
import { n as LIVE_FILE, o as liveCounts } from "./live-Bs3LReMV.js";
|
|
15
|
+
import { n as FLAGS, o as helpText, r as GLOBAL, t as COMMANDS } from "./cli-spec-DKAu09Gi.js";
|
|
16
|
+
import { i as liftLabel, r as gateLabel } from "./summary-D7Ttky0z.js";
|
|
17
|
+
import { n as gitInfo, r as runnerInfo, t as author } from "./provenance-Dfb8YY5Y.js";
|
|
18
|
+
import { t as budgetMarker } from "./budget-DBUrH4jX.js";
|
|
19
|
+
import { n as prepareAll, o as writePrepared, r as prepareTargets, t as RunStopError } from "./preflight-BypZMNBG.js";
|
|
20
|
+
import { r as imageHarness } from "./prepare-D6Uzcubb.js";
|
|
21
|
+
import { n as createProviders } from "./providers-2p3ylMHI.js";
|
|
22
|
+
import { a as redactArgv, r as commandOf, t as logInvocation } from "./invocations-rXktgSAn.js";
|
|
23
|
+
import { n as stripAnsi, t as colors } from "./colors-QV9dFVhN.js";
|
|
24
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { hostname } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { appendFile, readFile, rm } from "node:fs/promises";
|
|
28
|
+
import { inspect } from "node:util";
|
|
29
|
+
import { detectAgent } from "std-env";
|
|
30
|
+
import { Command, Option } from "commander";
|
|
31
|
+
//#region src/cli-parse.ts
|
|
32
|
+
const OPTIONS = Object.fromEntries(Object.entries(FLAGS).map(([name, spec]) => {
|
|
33
|
+
const short = "short" in spec ? `-${spec.short}, ` : "";
|
|
34
|
+
const value = spec.type === "string" ? " <value>" : "";
|
|
35
|
+
return [name, () => {
|
|
36
|
+
const option = new Option(`${short}--${name}${value}`);
|
|
37
|
+
if ("multiple" in spec && spec.multiple) option.argParser((v, prev = []) => [...prev, spec.type === "string" ? v : true]);
|
|
38
|
+
return option;
|
|
39
|
+
}];
|
|
40
|
+
}));
|
|
41
|
+
/**
|
|
42
|
+
* Parses `argv` against the CLI spec with commander. A flag belongs to the commands that list it
|
|
43
|
+
* (and `--config`, `--help` and `--version` to all of them); a flag of another command, a
|
|
44
|
+
* missing value or too many arguments is a UsageError. `--help` and `--version` skip the
|
|
45
|
+
* argument count, so the dispatcher can print them.
|
|
46
|
+
*/
|
|
47
|
+
function parseCli(argv) {
|
|
48
|
+
let parsed;
|
|
49
|
+
const program = configure(new Command("reeval"), "reeval", GLOBAL);
|
|
50
|
+
program.argument("[args...]").action(() => {
|
|
51
|
+
parsed = {
|
|
52
|
+
positionals: program.args,
|
|
53
|
+
values: valuesOf(program, GLOBAL)
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
for (const [name, spec] of Object.entries(COMMANDS)) {
|
|
57
|
+
const command = configure(program.command(name), `reeval ${name}`, spec.flags);
|
|
58
|
+
command.argument("[args...]").action(() => {
|
|
59
|
+
const max = spec.sub && !spec.help ? Number.POSITIVE_INFINITY : spec.positionals;
|
|
60
|
+
parsed = done(program, command, [name], spec.flags, max);
|
|
61
|
+
});
|
|
62
|
+
for (const [subName, sub] of Object.entries(spec.sub ?? {})) {
|
|
63
|
+
const subcommand = configure(command.command(subName), `reeval ${name} ${subName}`, sub.flags);
|
|
64
|
+
subcommand.argument("[args...]").action(() => {
|
|
65
|
+
const [stray] = Object.keys(valuesOf(command, spec.flags));
|
|
66
|
+
if (stray) throw new UsageError(`--${stray} isn't an option of reeval ${name} ${subName}`);
|
|
67
|
+
parsed = done(program, subcommand, [name, subName], sub.flags, sub.positionals);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
program.parse(argv, { from: "user" });
|
|
72
|
+
if (!parsed) throw new UsageError("Run reeval --help.");
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
function configure(command, name, flags) {
|
|
76
|
+
command.helpOption(false).helpCommand(false).allowExcessArguments().configureOutput({
|
|
77
|
+
writeOut: () => {},
|
|
78
|
+
writeErr: () => {},
|
|
79
|
+
outputError: () => {}
|
|
80
|
+
}).exitOverride((error) => {
|
|
81
|
+
throw usageError(error, name);
|
|
82
|
+
});
|
|
83
|
+
for (const flag of flags) command.addOption(OPTIONS[flag]());
|
|
84
|
+
return command;
|
|
85
|
+
}
|
|
86
|
+
function done(program, command, path, flags, max) {
|
|
87
|
+
const values = {
|
|
88
|
+
...valuesOf(program, GLOBAL),
|
|
89
|
+
...valuesOf(command, flags)
|
|
90
|
+
};
|
|
91
|
+
const args = command.args;
|
|
92
|
+
if (!values.help && !values.version && args.length > max) {
|
|
93
|
+
const name = `reeval ${path.join(" ")}`;
|
|
94
|
+
throw new UsageError(max === 0 ? `${name} takes no arguments (got ${args.join(" ")})` : `${name} takes at most ${max} argument${max === 1 ? "" : "s"} (got ${args.join(" ")})`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
positionals: [...path, ...args],
|
|
98
|
+
values
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** The flags given, under their spec names; a `--no-*` flag is true when given. */
|
|
102
|
+
function valuesOf(command, flags) {
|
|
103
|
+
const opts = command.opts();
|
|
104
|
+
const values = {};
|
|
105
|
+
for (const flag of flags) {
|
|
106
|
+
const option = command.options.find((o) => o.long === `--${flag}`);
|
|
107
|
+
const value = option && opts[option.attributeName()];
|
|
108
|
+
if (option?.negate) {
|
|
109
|
+
if (value === false) values[flag] = true;
|
|
110
|
+
} else if (value !== void 0) values[flag] = value;
|
|
111
|
+
}
|
|
112
|
+
return values;
|
|
113
|
+
}
|
|
114
|
+
/** Commander's error, in reeval's words, naming the command whose flag it is. */
|
|
115
|
+
function usageError(error, command) {
|
|
116
|
+
const [first = "", ...rest] = error.message.replace(/^error: /, "").split("\n");
|
|
117
|
+
if (error.code === "commander.unknownOption") {
|
|
118
|
+
const arg = /'([^']+)'/.exec(first)?.[1] ?? "";
|
|
119
|
+
const [flag = "", value] = arg.replace(/^--?/, "").split("=");
|
|
120
|
+
const known = arg.startsWith("--") ? FLAGS[flag] : Object.entries(FLAGS).find(([, s]) => "short" in s && s.short === flag)?.[1];
|
|
121
|
+
const long = arg.startsWith("--") ? flag : Object.entries(FLAGS).find(([, s]) => "short" in s && s.short === flag)?.[0];
|
|
122
|
+
if (known && value !== void 0 && known.type === "boolean") return new UsageError(`Option '--${flag}' does not take an argument`);
|
|
123
|
+
if (known) return new UsageError(`--${long} isn't an option of ${command}. Run reeval --help.`);
|
|
124
|
+
const hint = rest.join(" ").replace(/^\(|\)$/g, "");
|
|
125
|
+
return new UsageError(`Unknown option '${arg}'. ${hint ? `${hint} ` : ""}Run reeval --help.`);
|
|
126
|
+
}
|
|
127
|
+
return new UsageError(first.charAt(0).toUpperCase() + first.slice(1));
|
|
128
|
+
}
|
|
129
|
+
function finished(result) {
|
|
130
|
+
const outcome = result.meta?.outcome;
|
|
131
|
+
if (result.state === "skipped") return outcome === "fail" || outcome === "infra_error" ? outcome : "skipped";
|
|
132
|
+
if (outcome === "infra_error") return "infra_error";
|
|
133
|
+
return result.state === "passed" ? "pass" : "fail";
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Renames `from` over `to`. Windows refuses while a reader (`reeval studio`) has `to` open, for a
|
|
137
|
+
* moment: try again briefly rather than drop the update.
|
|
138
|
+
*/
|
|
139
|
+
function replace(from, to, rename = renameSync) {
|
|
140
|
+
for (let attempt = 1;; attempt++) try {
|
|
141
|
+
rename(from, to);
|
|
142
|
+
return;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const code = error.code;
|
|
145
|
+
if (attempt >= 20 || !(code === "EPERM" || code === "EACCES" || code === "EBUSY")) throw error;
|
|
146
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, attempt);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Writes the file at once, then on every change and heartbeat. Write errors never fail the run. */
|
|
150
|
+
function writeLiveProgress(options) {
|
|
151
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
152
|
+
const file = join(options.dir, LIVE_FILE);
|
|
153
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
154
|
+
const attempts = new Map(options.planned.map((p) => [p.key, {
|
|
155
|
+
key: p.key,
|
|
156
|
+
evalId: p.id,
|
|
157
|
+
name: p.attempt.name,
|
|
158
|
+
file: p.file,
|
|
159
|
+
cell: p.attempt.cell.key,
|
|
160
|
+
n: p.attempt.n,
|
|
161
|
+
status: p.attempt.plan.skip ? "skipped" : "queued",
|
|
162
|
+
startedAt: null,
|
|
163
|
+
durationMs: null
|
|
164
|
+
}]));
|
|
165
|
+
let phase = "running";
|
|
166
|
+
let stopped = false;
|
|
167
|
+
const read = () => {
|
|
168
|
+
const list = [...attempts.values()];
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
runId: options.runId,
|
|
172
|
+
startedAt: options.startedAt.toISOString(),
|
|
173
|
+
pid: process.pid,
|
|
174
|
+
host: hostname(),
|
|
175
|
+
heartbeatAt: now().toISOString(),
|
|
176
|
+
phase,
|
|
177
|
+
planned: list.length,
|
|
178
|
+
counts: liveCounts(list),
|
|
179
|
+
attempts: list
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
const write = () => {
|
|
183
|
+
if (stopped) return;
|
|
184
|
+
try {
|
|
185
|
+
writeFileSync(tmp, JSON.stringify(read()));
|
|
186
|
+
replace(tmp, file);
|
|
187
|
+
} catch {}
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
mkdirSync(options.dir, { recursive: true });
|
|
191
|
+
} catch {}
|
|
192
|
+
write();
|
|
193
|
+
const heartbeat = setInterval(write, options.heartbeatMs ?? 2e3);
|
|
194
|
+
heartbeat.unref();
|
|
195
|
+
const stop = () => {
|
|
196
|
+
stopped = true;
|
|
197
|
+
clearInterval(heartbeat);
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
start(key, at) {
|
|
201
|
+
const a = attempts.get(key);
|
|
202
|
+
if (!a) return;
|
|
203
|
+
a.status = "running";
|
|
204
|
+
a.startedAt = new Date(at).toISOString();
|
|
205
|
+
write();
|
|
206
|
+
},
|
|
207
|
+
end(result) {
|
|
208
|
+
const a = attempts.get(result.key);
|
|
209
|
+
if (!a) return;
|
|
210
|
+
a.status = finished(result);
|
|
211
|
+
a.durationMs = result.durationMs;
|
|
212
|
+
write();
|
|
213
|
+
},
|
|
214
|
+
phase(next) {
|
|
215
|
+
phase = next;
|
|
216
|
+
write();
|
|
217
|
+
},
|
|
218
|
+
stop,
|
|
219
|
+
remove() {
|
|
220
|
+
stop();
|
|
221
|
+
rmSync(file, { force: true });
|
|
222
|
+
rmSync(tmp, { force: true });
|
|
223
|
+
},
|
|
224
|
+
read
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/preflight.ts
|
|
229
|
+
/** How the terminal names a sandbox to prepare: its cell id, `+ install`, and a harness other than Cursor. */
|
|
230
|
+
function prepareLabel({ sandbox, harness }) {
|
|
231
|
+
if (sandbox.kind !== "docker") return sandboxId(sandbox);
|
|
232
|
+
const baked = imageHarness(harness);
|
|
233
|
+
const install = sandbox.install?.length ? " + install" : "";
|
|
234
|
+
return `${sandboxId(sandbox)}${install}${baked === "cursor" ? "" : ` · ${baked}`}`;
|
|
235
|
+
}
|
|
236
|
+
/** A run that stopped in preflight on an infra failure: exit 3. */
|
|
237
|
+
var PreflightInfraError = class extends Error {
|
|
238
|
+
name = "PreflightInfraError";
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Before any eval starts: checks each sandbox provider, then prepares every distinct sandbox the
|
|
242
|
+
* planned attempts need (building Docker images), in parallel, and writes the results for the
|
|
243
|
+
* run's workers. A config-level sandbox that fails stops the run (exit 2 for a mistake in the
|
|
244
|
+
* config, exit 3 otherwise); any other failure fails only the attempts that need it.
|
|
245
|
+
*/
|
|
246
|
+
async function preflight(uses, options) {
|
|
247
|
+
const providers = options.providers ?? createProviders();
|
|
248
|
+
const targets = prepareTargets(uses, providers, prepareLabel);
|
|
249
|
+
if (!targets.some((t) => t.kind !== "fake")) return { images: [] };
|
|
250
|
+
let outcomes;
|
|
251
|
+
try {
|
|
252
|
+
outcomes = await prepareAll(targets, {
|
|
253
|
+
providers,
|
|
254
|
+
root: options.root,
|
|
255
|
+
...options.signal ? { signal: options.signal } : {},
|
|
256
|
+
...options.delays ? { delays: options.delays } : {},
|
|
257
|
+
...options.onEvent ? { onEvent: options.onEvent } : {},
|
|
258
|
+
log: (line) => process.env.REEVAL_DEBUG && console.error(line)
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof PrepareError) throw error.failure.usage ? new UsageError(error.message) : new PreflightInfraError(error.message);
|
|
262
|
+
if (error instanceof RunStopError) throw error.outcome.failure.usage ? new UsageError(error.message) : new PreflightInfraError(error.message);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
writePrepared(options.root, options.runId, outcomes);
|
|
266
|
+
const images = [];
|
|
267
|
+
for (const o of outcomes) {
|
|
268
|
+
if (!o.ok || o.target.kind !== "docker") continue;
|
|
269
|
+
const { ref, id, agentCli } = o.handle;
|
|
270
|
+
if (images.some((i) => i.id === id)) continue;
|
|
271
|
+
const info = await imageInfo(ref);
|
|
272
|
+
images.push({
|
|
273
|
+
ref,
|
|
274
|
+
id,
|
|
275
|
+
repoDigests: info?.repoDigests ?? [],
|
|
276
|
+
agentCli: agentCli ?? info?.labels[LABEL.agentVersion] ?? null
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return { images };
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/ui/format.ts
|
|
283
|
+
/** Most detail lines a `-vv` payload prints under its line. */
|
|
284
|
+
const DEBUG_DETAIL_LINES = 40;
|
|
285
|
+
/**
|
|
286
|
+
* A debug line as the terminal shows it, `12:50:01 image cache hit …` (with milliseconds under
|
|
287
|
+
* `-vv`), then its detail indented under it.
|
|
288
|
+
*/
|
|
289
|
+
function debugLines(c, line, ms) {
|
|
290
|
+
const phase = line.phase.padEnd(9);
|
|
291
|
+
const head = `${c.dim(debugTime(line.at, ms))} ${c.dim(phase)} ${line.level === 2 ? c.dim(line.text) : line.text}`;
|
|
292
|
+
const detail = (line.detail ?? "").split("\n").filter((l) => l.trim());
|
|
293
|
+
const shown = detail.slice(0, DEBUG_DETAIL_LINES).map((l) => c.dim(` ${l}`));
|
|
294
|
+
const more = detail.length - shown.length;
|
|
295
|
+
return [
|
|
296
|
+
head,
|
|
297
|
+
...shown,
|
|
298
|
+
...more > 0 ? [c.dim(` … ${more} more lines in trace.ndjson`)] : []
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
/** Offset into an attempt, `+1:05`. */
|
|
302
|
+
function offset(ms) {
|
|
303
|
+
const s = Math.max(0, Math.floor(ms / 1e3));
|
|
304
|
+
return `+${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
305
|
+
}
|
|
306
|
+
function outcomeOf(e) {
|
|
307
|
+
if (e.state === "skipped") return "skipped";
|
|
308
|
+
if (e.meta?.errorKind === "aborted") return "cancelled";
|
|
309
|
+
return e.meta?.outcome ?? (e.state === "passed" ? "pass" : "fail");
|
|
310
|
+
}
|
|
311
|
+
function attemptOutcome(a) {
|
|
312
|
+
return a.error?.kind === "aborted" ? "cancelled" : a.outcome;
|
|
313
|
+
}
|
|
314
|
+
/** What tells an eval's attempts apart: the axes that vary (`cursor:composer-2.5 · with-skill`), else the harness. */
|
|
315
|
+
function attemptAxes(a, fallback) {
|
|
316
|
+
return a?.label.replace(/\s*#\d+$/, "") || a?.harness.kind || fallback;
|
|
317
|
+
}
|
|
318
|
+
function mark(c, outcome) {
|
|
319
|
+
return {
|
|
320
|
+
pass: c.green("✓"),
|
|
321
|
+
fail: c.red("✗"),
|
|
322
|
+
infra_error: c.yellow("!"),
|
|
323
|
+
skipped: c.dim("↓"),
|
|
324
|
+
cancelled: c.yellow("✗")
|
|
325
|
+
}[outcome];
|
|
326
|
+
}
|
|
327
|
+
/** An expected miss's mark: neutral, since it doesn't fail the run. */
|
|
328
|
+
const MISS_MARK = (c) => c.dim("○");
|
|
329
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
330
|
+
/** A command on one line: the first line, cut to 80 characters at a word boundary. */
|
|
331
|
+
function oneLine(text, max = 80) {
|
|
332
|
+
const [first = ""] = text.trim().split("\n");
|
|
333
|
+
const cut = clip(first, max);
|
|
334
|
+
return cut === first && text.trim().includes("\n") ? `${first}…` : cut;
|
|
335
|
+
}
|
|
336
|
+
/** One line per worker progress event, without prefix or offset. */
|
|
337
|
+
function progressLine(c, p) {
|
|
338
|
+
switch (p.kind) {
|
|
339
|
+
case "sandbox": return c.dim("sandbox starting");
|
|
340
|
+
case "sandbox-ready": return `sandbox ready ${c.dim(formatDuration(p.ms))}`;
|
|
341
|
+
case "turn-start": return `turn ${p.turn} started`;
|
|
342
|
+
case "turn-end": {
|
|
343
|
+
const status = p.status === "completed" ? "done" : c.red(p.status);
|
|
344
|
+
const activity = [plural(p.tools, "tool call"), plural(p.fileEdits, "file edit")].join(", ");
|
|
345
|
+
const error = p.error ? `: ${c.red(oneLine(p.error, 160))}` : "";
|
|
346
|
+
return `turn ${p.turn} ${status}${error} ${c.dim(`${formatDuration(p.ms)} · ${activity}`)}`;
|
|
347
|
+
}
|
|
348
|
+
case "check-start": return c.dim(`$ ${oneLine(p.command)}`);
|
|
349
|
+
case "check-end": {
|
|
350
|
+
const exit = p.exitCode === 0 ? c.dim("exit 0") : c.red(`exit ${p.exitCode}`);
|
|
351
|
+
return `$ ${oneLine(p.command)} ${exit} ${c.dim(formatDuration(p.ms))}`;
|
|
352
|
+
}
|
|
353
|
+
case "judge-start": return c.dim(`judging: ${oneLine(p.question, 120)}`);
|
|
354
|
+
case "judge-end": return judgeLine(c, p);
|
|
355
|
+
case "cleanup": return c.dim("cleaning up");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function judgeLine(c, j) {
|
|
359
|
+
const verdict = j.verdict === "pass" ? c.green("pass") : c.red(j.verdict);
|
|
360
|
+
const score = j.score === null ? "" : ` ${j.score.toFixed(2)}`;
|
|
361
|
+
const threshold = j.threshold === null ? "" : ` ${c.dim(`(≥ ${j.threshold})`)}`;
|
|
362
|
+
return `${c.dim("judge")} ${verdict}${score}${threshold} ${j.question}`;
|
|
363
|
+
}
|
|
364
|
+
/** The phase a running attempt is in, for the heartbeat. */
|
|
365
|
+
function phaseOf(p) {
|
|
366
|
+
switch (p?.kind) {
|
|
367
|
+
case void 0: return "starting";
|
|
368
|
+
case "sandbox": return "sandbox";
|
|
369
|
+
case "sandbox-ready": return "sandbox ready";
|
|
370
|
+
case "turn-start": return `turn ${p.turn}`;
|
|
371
|
+
case "turn-end": return `turn ${p.turn} done`;
|
|
372
|
+
case "check-start":
|
|
373
|
+
case "check-end": return "checks";
|
|
374
|
+
case "judge-start":
|
|
375
|
+
case "judge-end": return "judging";
|
|
376
|
+
case "cleanup": return "cleanup";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function capLines(text, max) {
|
|
380
|
+
const lines = text.replace(/\s+$/, "").split("\n");
|
|
381
|
+
if (lines.length <= max) return lines;
|
|
382
|
+
return [...lines.slice(0, max), `(+${lines.length - max} more lines)`];
|
|
383
|
+
}
|
|
384
|
+
/** An infra error's headline, then its last lines: the cause is at the end. */
|
|
385
|
+
function tailLines(text, max) {
|
|
386
|
+
const lines = text.replace(/\s+$/, "").split("\n");
|
|
387
|
+
if (lines.length <= max) return lines;
|
|
388
|
+
const rest = Math.max(1, max - 1);
|
|
389
|
+
return [
|
|
390
|
+
lines[0] ?? "",
|
|
391
|
+
`(${lines.length - 1 - rest} more lines)`,
|
|
392
|
+
...lines.slice(-rest)
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Why an eval failed, as lines: the judge verdict (and reason) for judge failures, Vitest's
|
|
397
|
+
* formatted error (diff, code frame) for assertions, the tail of the message for infra errors.
|
|
398
|
+
*/
|
|
399
|
+
function failureLines(c, e, options) {
|
|
400
|
+
const outcome = outcomeOf(e);
|
|
401
|
+
if (outcome === "cancelled") return [c.yellow("cancelled")];
|
|
402
|
+
const message = e.errors[0] ?? "";
|
|
403
|
+
const turns = (e.meta?.turnErrors ?? []).filter((t) => !message.includes(t.replace(/^turn \d+ error: /, ""))).map((t) => c.yellow(t));
|
|
404
|
+
return [...failureBody(c, e, outcome, message, options), ...turns];
|
|
405
|
+
}
|
|
406
|
+
function failureBody(c, e, outcome, message, options) {
|
|
407
|
+
if (outcome === "infra_error") {
|
|
408
|
+
if (e.meta?.prepare) {
|
|
409
|
+
const lines = message.trimEnd().split("\n");
|
|
410
|
+
return [
|
|
411
|
+
c.yellow("infra error"),
|
|
412
|
+
lines[0] ?? "",
|
|
413
|
+
lines.at(-1) ?? ""
|
|
414
|
+
].filter((l, i, all) => all.indexOf(l) === i);
|
|
415
|
+
}
|
|
416
|
+
return [`${c.yellow("infra error")}`, ...tailLines(message, options.max)];
|
|
417
|
+
}
|
|
418
|
+
const failed = (e.meta?.judgements ?? []).filter((j) => j.verdict !== "pass");
|
|
419
|
+
if (e.meta?.errorKind === "judge" && failed.length) return failed.flatMap((j, i) => {
|
|
420
|
+
const reason = options.reasons?.[i];
|
|
421
|
+
return [judgeLine(c, {
|
|
422
|
+
...j,
|
|
423
|
+
threshold: j.threshold
|
|
424
|
+
}), ...reason ? [` ${reason}`] : []];
|
|
425
|
+
});
|
|
426
|
+
const formatted = e.formattedErrors[0];
|
|
427
|
+
const body = formatted && e.meta?.errorKind !== "timeout" ? formatted : message;
|
|
428
|
+
return capLines((c.enabled ? body : stripAnsi(body)).replace(/^\n+/, ""), options.max).map((l) => c.enabled && body === message ? c.red(l) : l);
|
|
429
|
+
}
|
|
430
|
+
/** Preflight's lines: which sandboxes it prepares, the slow ones when ready, and each failure. */
|
|
431
|
+
function prepareLines(c, e, max) {
|
|
432
|
+
if (e.state === "start") {
|
|
433
|
+
const n = e.targets.length;
|
|
434
|
+
const names = e.targets.map((t) => t.label).join(", ");
|
|
435
|
+
return [c.dim(`preparing ${n} sandbox${n === 1 ? "" : "es"}: ${names}`)];
|
|
436
|
+
}
|
|
437
|
+
const { outcome } = e;
|
|
438
|
+
if (outcome.ok) return outcome.ms >= 1e3 ? [c.dim(`${c.green("✓")} ${outcome.target.label} ready ${formatDuration(outcome.ms)}`)] : [];
|
|
439
|
+
const who = outcome.target.setBy.join(", ");
|
|
440
|
+
return [`${c.red("✗")} ${outcome.target.label} failed to prepare ${c.dim(`(set by ${who}; only its attempts fail)`)}`, ...tailLines(outcome.failure.message, max).map((l) => ` ${c.yellow(l)}`)];
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/ui/summary.ts
|
|
444
|
+
const FAILURE_LINES = 30;
|
|
445
|
+
const KINDS = [
|
|
446
|
+
{
|
|
447
|
+
title: "Judge failures",
|
|
448
|
+
test: (a) => a.error?.kind === "judge"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
title: "Assertion failures",
|
|
452
|
+
test: (a) => a.error?.kind === "assertion"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
title: "Timeouts",
|
|
456
|
+
test: (a) => a.error?.kind === "timeout"
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
title: "Infra errors",
|
|
460
|
+
test: (_, o) => o === "infra_error"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
title: "Cancelled",
|
|
464
|
+
test: (_, o) => o === "cancelled"
|
|
465
|
+
}
|
|
466
|
+
];
|
|
467
|
+
/** The engine's result for an attempt, or one rebuilt from the record (e.g. cancelled ones). */
|
|
468
|
+
function resultFor(entry, attempt, results) {
|
|
469
|
+
const key = attemptKey(entry.id, attempt.cell, attempt.n);
|
|
470
|
+
const found = results.find((r) => r.key === key);
|
|
471
|
+
const errorKind = attempt.error?.kind ?? null;
|
|
472
|
+
const meta = {
|
|
473
|
+
outcome: attempt.outcome === "skipped" ? "pass" : attempt.outcome,
|
|
474
|
+
errorKind,
|
|
475
|
+
judgements: attempt.judgements.map((j) => ({ ...j })),
|
|
476
|
+
turnErrors: attempt.turns.flatMap((t, i) => t.error ? [`turn ${i + 1} error: ${t.error}`] : []),
|
|
477
|
+
recordPath: ""
|
|
478
|
+
};
|
|
479
|
+
if (found) return {
|
|
480
|
+
...found,
|
|
481
|
+
meta: found.meta ? {
|
|
482
|
+
...found.meta,
|
|
483
|
+
errorKind
|
|
484
|
+
} : meta
|
|
485
|
+
};
|
|
486
|
+
return {
|
|
487
|
+
id: entry.id,
|
|
488
|
+
key,
|
|
489
|
+
file: entry.file,
|
|
490
|
+
group: entry.group,
|
|
491
|
+
name: entry.name,
|
|
492
|
+
attempt: void 0,
|
|
493
|
+
state: attempt.outcome === "pass" ? "passed" : attempt.outcome === "skipped" ? "skipped" : "failed",
|
|
494
|
+
durationMs: attempt.durationMs,
|
|
495
|
+
errors: attempt.error ? [attempt.error.message] : [],
|
|
496
|
+
formattedErrors: [],
|
|
497
|
+
meta
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function entryOutcome(e) {
|
|
501
|
+
const outcomes = e.attempts.map(attemptOutcome);
|
|
502
|
+
if (outcomes.length && outcomes.every((o) => o === "cancelled")) return "cancelled";
|
|
503
|
+
return e.outcome;
|
|
504
|
+
}
|
|
505
|
+
const pct = (rate) => rate === null ? " – " : `${Math.round(rate * 100)}%`.padStart(4);
|
|
506
|
+
function evalColumns(c, e) {
|
|
507
|
+
const [attempt, ...more] = e.attempts;
|
|
508
|
+
if (e.todo) return c.dim("todo");
|
|
509
|
+
if (!attempt || e.outcome === "skipped") return c.dim("skipped");
|
|
510
|
+
if (more.length) {
|
|
511
|
+
const pass = e.attempts.filter((a) => a.outcome === "pass").length;
|
|
512
|
+
const counted = e.attempts.filter((a) => a.outcome === "pass" || a.outcome === "fail").length;
|
|
513
|
+
const rate = hasUngatedCells(e.cells) ? ` · ${pct(e.passRate).trim()} gated` : ` ${pct(e.passRate)}`;
|
|
514
|
+
return c.dim(`${`${pass}/${counted}`.padStart(7)}${rate} ${e.attempts.length} attempts`);
|
|
515
|
+
}
|
|
516
|
+
if (attemptOutcome(attempt) === "cancelled") return c.yellow("cancelled");
|
|
517
|
+
const cols = [formatDuration(attempt.durationMs).padStart(7)];
|
|
518
|
+
const scores = attempt.judgements.map((j) => j.score);
|
|
519
|
+
if (scores.length) cols.push(`judge ${Math.min(...scores).toFixed(2)}`);
|
|
520
|
+
const turns = attempt.turns.length;
|
|
521
|
+
cols.push(`${turns} turn${turns === 1 ? "" : "s"}`);
|
|
522
|
+
return c.dim(cols.join(" "));
|
|
523
|
+
}
|
|
524
|
+
/** Pass rate per cell for evals with several attempts; with compare, each cell's role and lift. */
|
|
525
|
+
function cellRows(c, e, indent) {
|
|
526
|
+
if (e.attempts.length < 2) return [];
|
|
527
|
+
const compare = e.cells.some((cell) => cell.role === "variant");
|
|
528
|
+
const width = Math.max(...e.cells.map((cell) => cell.key.length));
|
|
529
|
+
return e.cells.flatMap((cell) => {
|
|
530
|
+
const role = compare ? ` ${cell.role.padEnd(7)}` : "";
|
|
531
|
+
const counts = `${cell.pass}/${cell.pass + cell.fail}`.padStart(5);
|
|
532
|
+
const infra = cell.infraError ? c.yellow(` ${cell.infraError} infra`) : "";
|
|
533
|
+
let note = "";
|
|
534
|
+
if (cell.lift) {
|
|
535
|
+
const { minLift } = cell.lift;
|
|
536
|
+
const below = minLift !== void 0 && cell.lift.interval.lo < minLift;
|
|
537
|
+
const text = `${liftLabel(cell.lift)}${minLift !== void 0 ? ` (minLift ${Math.round(minLift * 100)})` : ""}`;
|
|
538
|
+
note = ` ${below ? c.red(`${text} ✗`) : text}`;
|
|
539
|
+
} else if (compare && cell.gate === "none" && cell.fail > 0) note = ` ${c.dim("not gated")}`;
|
|
540
|
+
if (cell.gate !== "all" && cell.gate !== "none") note += ` ${c.dim(`gate ${gateLabel(cell.gate)}`)}`;
|
|
541
|
+
const rows = [`${indent}${c.dim(cell.key.padEnd(width))}${role} ${counts} ${pct(cell.passRate)}${note}${infra}`];
|
|
542
|
+
const effort = cellContrast(e, cell.key);
|
|
543
|
+
if (effort) rows.push(`${indent} ${c.dim(`effort: ${effortLabel(effort)}`)}`);
|
|
544
|
+
if (cell.overrides.length) rows.push(`${indent} ${c.dim(`overrides: ${cell.overrides.join(", ")}`)}`);
|
|
545
|
+
return rows;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/** The run's evals as a file › group tree, one row per eval and per cell of a matrix. */
|
|
549
|
+
function tree(c, evals) {
|
|
550
|
+
const rows = [];
|
|
551
|
+
let path = [];
|
|
552
|
+
for (const e of evals) {
|
|
553
|
+
const next = [e.file, ...e.group];
|
|
554
|
+
let same = 0;
|
|
555
|
+
while (same < path.length && same < next.length && path[same] === next[same]) same++;
|
|
556
|
+
for (let i = same; i < next.length; i++) rows.push({
|
|
557
|
+
indent: i,
|
|
558
|
+
text: i === 0 ? c.bold(next[i] ?? "") : next[i] ?? ""
|
|
559
|
+
});
|
|
560
|
+
path = next;
|
|
561
|
+
rows.push({
|
|
562
|
+
indent: next.length,
|
|
563
|
+
text: e.name,
|
|
564
|
+
entry: e
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const width = Math.max(...rows.filter((r) => r.entry).map((r) => r.indent * 2 + 2 + r.text.length));
|
|
568
|
+
return rows.flatMap(({ indent, text, entry }) => {
|
|
569
|
+
const pad = " ".repeat(indent);
|
|
570
|
+
if (!entry) return [` ${pad}${text}`];
|
|
571
|
+
return [` ${pad}${`${mark(c, entryOutcome(entry))} ${text}`}${" ".repeat(Math.max(0, width - indent * 2 - 2 - text.length))} ${evalColumns(c, entry)}`, ...cellRows(c, entry, ` ${pad} `)];
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function totals(c, s, cancelled = 0) {
|
|
575
|
+
const fail = s.fail - s.notGated - cancelled;
|
|
576
|
+
const parts = [
|
|
577
|
+
s.pass && c.green(`${s.pass} passed`),
|
|
578
|
+
fail > 0 && c.red(`${fail} failed`),
|
|
579
|
+
s.notGated > 0 && c.dim(`${s.notGated} missed, not gated`),
|
|
580
|
+
s.infraError && c.yellow(`${s.infraError} infra error`),
|
|
581
|
+
cancelled && c.yellow(`${cancelled} cancelled`),
|
|
582
|
+
s.skipped && c.dim(`${s.skipped} skipped`)
|
|
583
|
+
].filter(Boolean);
|
|
584
|
+
const scope = s.attempts === s.evals ? `${s.evals}` : `${s.attempts} attempts, ${s.evals} evals`;
|
|
585
|
+
return parts.length ? `${parts.join(c.dim(" · "))} ${c.dim(`(${scope})`)}` : "no evals ran";
|
|
586
|
+
}
|
|
587
|
+
/** Failing attempts, in run order, including expected misses. */
|
|
588
|
+
function failingAttempts(evals) {
|
|
589
|
+
return evals.flatMap((entry) => entry.attempts.map((attempt) => ({
|
|
590
|
+
entry,
|
|
591
|
+
attempt,
|
|
592
|
+
outcome: attemptOutcome(attempt)
|
|
593
|
+
})).filter(({ outcome }) => outcome !== "pass" && outcome !== "skipped"));
|
|
594
|
+
}
|
|
595
|
+
function renderSummary(c, e, attemptDir) {
|
|
596
|
+
const { record } = e;
|
|
597
|
+
const lines = [];
|
|
598
|
+
if (record.evals.length) lines.push(c.bold("Summary"), ...tree(c, record.evals));
|
|
599
|
+
const all = failingAttempts(record.evals);
|
|
600
|
+
const missed = all.filter(({ entry, attempt }) => isUngatedMiss(entry, attempt));
|
|
601
|
+
const failing = all.filter((f) => !missed.includes(f));
|
|
602
|
+
if (failing.length) {
|
|
603
|
+
lines.push("", c.bold(c.red(`Failures (${failing.length})`)));
|
|
604
|
+
for (const kind of KINDS) {
|
|
605
|
+
const members = failing.filter(({ attempt, outcome }) => kind.test(attempt, outcome));
|
|
606
|
+
if (!members.length) continue;
|
|
607
|
+
lines.push("", c.bold(`${kind.title} (${members.length})`));
|
|
608
|
+
for (const { entry, attempt, outcome } of members) {
|
|
609
|
+
const result = resultFor(entry, attempt, e.results);
|
|
610
|
+
const which = entry.attempts.length > 1 ? ` ${c.dim(attempt.id)}` : "";
|
|
611
|
+
const gated = entry.cells.find((x) => x.key === attempt.cell)?.gate !== "none";
|
|
612
|
+
lines.push(` ${mark(c, outcome)} ${entry.id}${which}${gated ? "" : ` ${c.dim("(not gated)")}`}`);
|
|
613
|
+
if (outcome !== "cancelled") for (const line of failureLines(c, result, { max: FAILURE_LINES })) lines.push(` ${line}`);
|
|
614
|
+
if (!attempt.error?.prepare) lines.push(` ${c.dim("record")} ${attemptDir(entry.id, attempt.cell, attempt.n)}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (missed.length) {
|
|
619
|
+
lines.push("", c.bold(`Expected misses, not gated (${missed.length})`));
|
|
620
|
+
for (const { entry, attempt } of missed) {
|
|
621
|
+
const which = entry.attempts.length > 1 ? ` ${c.dim(attempt.id)}` : "";
|
|
622
|
+
const [reason] = failureLines(colors(false), resultFor(entry, attempt, e.results), { max: 1 }).filter((l) => l.trim());
|
|
623
|
+
lines.push(` ${MISS_MARK(c)} ${entry.id}${which}`);
|
|
624
|
+
if (reason) lines.push(` ${c.dim(reason.trim())}`);
|
|
625
|
+
lines.push(` ${c.dim("record")} ${attemptDir(entry.id, attempt.cell, attempt.n)}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (const error of e.unhandledErrors) lines.push("", c.red(`error: ${error}`));
|
|
629
|
+
const cancelled = failing.filter((f) => f.outcome === "cancelled").length;
|
|
630
|
+
const label = record.summary.attempts === record.summary.evals ? " Evals" : "Attempts";
|
|
631
|
+
const lifted = record.evals.flatMap((entry) => entry.cells.flatMap((cell) => cell.lift?.minLift !== void 0 && cell.lift.interval.lo < cell.lift.minLift ? [`${entry.id} ${cell.key} ${liftLabel(cell.lift)}: its lower bound is below minLift ${Math.round(cell.lift.minLift * 100)}`] : []));
|
|
632
|
+
if (lifted.length) {
|
|
633
|
+
lines.push("", c.bold(c.red(`Lift below minLift (${lifted.length})`)));
|
|
634
|
+
for (const line of lifted) lines.push(` ${c.red("✗")} ${line}`);
|
|
635
|
+
}
|
|
636
|
+
if (record.status === "budget") lines.push("", c.yellow("Budget reached: attempts that hadn’t started were skipped"));
|
|
637
|
+
lines.push("", `${c.dim(label)} ${totals(c, record.summary, cancelled)}`, `${c.dim("Duration")} ${formatDuration(record.durationMs)}`);
|
|
638
|
+
const tokens = formatUsage(record.cost.usage);
|
|
639
|
+
if (tokens) lines.push(` ${c.dim("Tokens")} ${tokens}`);
|
|
640
|
+
lines.push(` ${c.dim("Results")} ${e.summaryPath}`);
|
|
641
|
+
return lines;
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/ui/tags.ts
|
|
645
|
+
const STOPWORDS = new Set("a an the and or of to in on for with without from by at as is are be it its that this when than then into via not no".split(" "));
|
|
646
|
+
const MAX_WORDS = 3;
|
|
647
|
+
const MAX_LENGTH = 24;
|
|
648
|
+
const words = (name) => name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w && !STOPWORDS.has(w));
|
|
649
|
+
/**
|
|
650
|
+
* Short handles for the evals of one file, e.g. `greet`: the first distinctive words of each
|
|
651
|
+
* name, where words shared by most of the file's evals ("recommends Genkit over …") don't
|
|
652
|
+
* count. Depends only on the file's evals, so a tag stays the same across runs and filters.
|
|
653
|
+
*/
|
|
654
|
+
function fileTags(evals, taken) {
|
|
655
|
+
const lists = evals.map((e) => words(e.name));
|
|
656
|
+
const df = /* @__PURE__ */ new Map();
|
|
657
|
+
for (const list of lists) for (const w of new Set(list)) df.set(w, (df.get(w) ?? 0) + 1);
|
|
658
|
+
const common = (w) => evals.length > 1 && (df.get(w) ?? 0) > evals.length / 2;
|
|
659
|
+
const candidates = lists.map((list) => {
|
|
660
|
+
const rare = list.filter((w) => !common(w));
|
|
661
|
+
return rare.length ? rare : list.length ? list : ["eval"];
|
|
662
|
+
});
|
|
663
|
+
const tags = /* @__PURE__ */ new Map();
|
|
664
|
+
const clip = (tag) => tag.slice(0, MAX_LENGTH).replace(/-+$/, "");
|
|
665
|
+
const prefix = (list, n) => clip(list.slice(0, n).join("-"));
|
|
666
|
+
evals.forEach((e, i) => {
|
|
667
|
+
const own = candidates[i] ?? ["eval"];
|
|
668
|
+
let tag = prefix(own, 1);
|
|
669
|
+
for (let n = 1; n <= MAX_WORDS; n++) {
|
|
670
|
+
tag = prefix(own, n);
|
|
671
|
+
if (!candidates.some((other, j) => j !== i && prefix(other, n) === tag) && !taken.has(tag)) break;
|
|
672
|
+
}
|
|
673
|
+
let unique = tag;
|
|
674
|
+
for (let k = 2; taken.has(unique); k++) unique = `${tag}-${k}`;
|
|
675
|
+
taken.add(unique);
|
|
676
|
+
tags.set(e.id, unique);
|
|
677
|
+
});
|
|
678
|
+
return tags;
|
|
679
|
+
}
|
|
680
|
+
/** Tags evals as their files are collected; later files never rename earlier tags. */
|
|
681
|
+
var Tagger = class {
|
|
682
|
+
#tags = /* @__PURE__ */ new Map();
|
|
683
|
+
#taken = /* @__PURE__ */ new Set();
|
|
684
|
+
addFile(attempts) {
|
|
685
|
+
const fresh = [...new Map(attempts.map((a) => [a.id, a])).values()].filter((e) => !this.#tags.has(e.id));
|
|
686
|
+
for (const [id, tag] of fileTags(fresh, this.#taken)) this.#tags.set(id, tag);
|
|
687
|
+
}
|
|
688
|
+
/** `greet#1`: the eval's tag and run number. */
|
|
689
|
+
label(id, attempt) {
|
|
690
|
+
let tag = this.#tags.get(id);
|
|
691
|
+
if (!tag) {
|
|
692
|
+
this.addFile([{
|
|
693
|
+
id,
|
|
694
|
+
name: id.split(" › ").at(-1) ?? id
|
|
695
|
+
}]);
|
|
696
|
+
tag = this.#tags.get(id) ?? "eval";
|
|
697
|
+
}
|
|
698
|
+
return `${tag}#${attempt}`;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/ui/agent.ts
|
|
703
|
+
const MAX_LINES = 12;
|
|
704
|
+
const WORD = {
|
|
705
|
+
fail: "FAIL",
|
|
706
|
+
infra_error: "INFRA",
|
|
707
|
+
cancelled: "CANCELLED"
|
|
708
|
+
};
|
|
709
|
+
var AgentRenderer = class {
|
|
710
|
+
#o;
|
|
711
|
+
#c = colors(false);
|
|
712
|
+
#logs = /* @__PURE__ */ new Map();
|
|
713
|
+
#printed = /* @__PURE__ */ new Set();
|
|
714
|
+
/** `[key]` prefixes for `-v` lines, from each attempt's start. */
|
|
715
|
+
#labels = /* @__PURE__ */ new Map();
|
|
716
|
+
#tags = new Tagger();
|
|
717
|
+
constructor(options) {
|
|
718
|
+
this.#o = options;
|
|
719
|
+
}
|
|
720
|
+
#write(text) {
|
|
721
|
+
this.#o.out.write(`${text.trimEnd()}\n`);
|
|
722
|
+
}
|
|
723
|
+
#failure(result, cell, n) {
|
|
724
|
+
const outcome = outcomeOf(result);
|
|
725
|
+
if (outcome === "pass" || outcome === "skipped") return;
|
|
726
|
+
this.#printed.add(result.key);
|
|
727
|
+
const kind = outcome === "fail" ? result.meta?.errorKind ?? "assertion" : null;
|
|
728
|
+
const detail = [
|
|
729
|
+
formatDuration(result.durationMs),
|
|
730
|
+
kind,
|
|
731
|
+
result.attempt?.gate === "none" ? "not gated" : ""
|
|
732
|
+
].filter(Boolean).join(", ");
|
|
733
|
+
const which = result.attempt?.label ? ` [${result.attempt.label}]` : "";
|
|
734
|
+
const missed = outcome === "fail" && result.attempt?.gate === "none";
|
|
735
|
+
this.#write(`${missed ? "MISS" : WORD[outcome]} ${result.id}${which} (${detail})`);
|
|
736
|
+
if (outcome !== "cancelled") {
|
|
737
|
+
const lines = failureLines(this.#c, result, { max: MAX_LINES });
|
|
738
|
+
for (const line of missed ? lines.filter((l) => l.trim()).slice(0, 1) : lines) this.#write(` ${line}`);
|
|
739
|
+
}
|
|
740
|
+
const logs = missed ? void 0 : this.#logs.get(result.key);
|
|
741
|
+
if (logs?.length) {
|
|
742
|
+
this.#write(" console:");
|
|
743
|
+
for (const line of capLines(logs.join("\n"), MAX_LINES)) this.#write(` ${line}`);
|
|
744
|
+
}
|
|
745
|
+
if (!result.meta?.prepare) this.#write(` record: ${this.#o.attemptDir(result.id, cell, n)}`);
|
|
746
|
+
}
|
|
747
|
+
onEvent(e) {
|
|
748
|
+
switch (e.type) {
|
|
749
|
+
case "plan":
|
|
750
|
+
for (const line of e.lines) this.#write(line);
|
|
751
|
+
for (const w of e.sandboxWarnings ?? []) this.#write(sandboxWarningLine(w));
|
|
752
|
+
return;
|
|
753
|
+
case "run-start":
|
|
754
|
+
this.#write(`reeval run: ${e.files} eval file${e.files === 1 ? "" : "s"}`);
|
|
755
|
+
return;
|
|
756
|
+
case "collected":
|
|
757
|
+
this.#tags.addFile(e.evals);
|
|
758
|
+
return;
|
|
759
|
+
case "eval-start": {
|
|
760
|
+
const tag = this.#tags.label(e.id, e.attempt?.n ?? 1);
|
|
761
|
+
this.#labels.set(e.key, `[${tag} ${attemptAxes(e.attempt, "agent")}]`);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
case "debug": {
|
|
765
|
+
const label = this.#labels.get(e.key) ?? `[${e.key}]`;
|
|
766
|
+
for (const line of debugLines(this.#c, e.line, this.#o.verbosity === 2)) this.#write(`${label} ${line}`);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
case "log": {
|
|
770
|
+
if (!e.key) return;
|
|
771
|
+
const logs = this.#logs.get(e.key) ?? [];
|
|
772
|
+
logs.push(...e.text.replace(/\n$/, "").split("\n"));
|
|
773
|
+
this.#logs.set(e.key, logs);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
case "eval-end": {
|
|
777
|
+
const { attempt } = e.result;
|
|
778
|
+
this.#failure(e.result, attempt?.cell.key ?? "", attempt?.n ?? 1);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
case "prepare":
|
|
782
|
+
for (const line of prepareLines(this.#c, e, MAX_LINES)) this.#write(line);
|
|
783
|
+
return;
|
|
784
|
+
case "notice":
|
|
785
|
+
if (e.level !== "info") this.#write(e.text);
|
|
786
|
+
return;
|
|
787
|
+
case "run-end": {
|
|
788
|
+
const failing = failingAttempts(e.record.evals);
|
|
789
|
+
for (const { entry, attempt, outcome } of failing) {
|
|
790
|
+
const result = resultFor(entry, attempt, e.results);
|
|
791
|
+
if (outcome === "cancelled" && !this.#printed.has(result.key)) this.#failure(result, attempt.cell, attempt.n);
|
|
792
|
+
}
|
|
793
|
+
for (const error of e.unhandledErrors) this.#write(`ERROR ${error}`);
|
|
794
|
+
const s = e.record.summary;
|
|
795
|
+
const cancelled = failing.filter((f) => f.outcome === "cancelled").length;
|
|
796
|
+
const parts = [
|
|
797
|
+
s.pass && `${s.pass} passed`,
|
|
798
|
+
s.fail - s.notGated - cancelled > 0 && `${s.fail - s.notGated - cancelled} failed`,
|
|
799
|
+
s.notGated > 0 && `${s.notGated} missed, not gated`,
|
|
800
|
+
s.infraError && `${s.infraError} infra error`,
|
|
801
|
+
cancelled && `${cancelled} cancelled`,
|
|
802
|
+
s.skipped && `${s.skipped} skipped`
|
|
803
|
+
].filter(Boolean);
|
|
804
|
+
const scope = s.attempts === s.evals ? "" : ` (${s.attempts} attempts, ${s.evals} evals)`;
|
|
805
|
+
this.#write(parts.length ? `${parts.join(", ")}${scope}` : "no evals ran");
|
|
806
|
+
this.#write(`summary: ${e.summaryPath}`);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
default: return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
close() {}
|
|
813
|
+
};
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/ui/plain.ts
|
|
816
|
+
/** In a grouped block, an end line replaces its start line and keeps its start time. */
|
|
817
|
+
const STARTS = {
|
|
818
|
+
"sandbox-ready": "sandbox",
|
|
819
|
+
"turn-end": "turn-start",
|
|
820
|
+
"check-end": "check-start",
|
|
821
|
+
"judge-end": "judge-start"
|
|
822
|
+
};
|
|
823
|
+
const ERROR_LINES = 12;
|
|
824
|
+
/** GitHub Actions workflow-command escaping. */
|
|
825
|
+
const ghEscape = (s, property = false) => {
|
|
826
|
+
const out = s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
827
|
+
return property ? out.replace(/:/g, "%3A").replace(/,/g, "%2C") : out;
|
|
828
|
+
};
|
|
829
|
+
/** The eval file's line in Vitest's formatted error (`❯ evals/a.eval.ts:7:33`), if any. */
|
|
830
|
+
function locate(result) {
|
|
831
|
+
const text = stripAnsi(result.formattedErrors[0] ?? "");
|
|
832
|
+
const file = result.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
833
|
+
const match = new RegExp(`❯ ${file}:(\\d+):(\\d+)`).exec(text);
|
|
834
|
+
return match?.[1] && match[2] ? {
|
|
835
|
+
line: match[1],
|
|
836
|
+
col: match[2]
|
|
837
|
+
} : void 0;
|
|
838
|
+
}
|
|
839
|
+
var PlainRenderer = class {
|
|
840
|
+
#o;
|
|
841
|
+
#c;
|
|
842
|
+
#tags = new Tagger();
|
|
843
|
+
#running = /* @__PURE__ */ new Map();
|
|
844
|
+
#now;
|
|
845
|
+
#queued = /* @__PURE__ */ new Set();
|
|
846
|
+
#runStartedAt = 0;
|
|
847
|
+
#lastWrite = 0;
|
|
848
|
+
#timer;
|
|
849
|
+
constructor(options) {
|
|
850
|
+
this.#o = options;
|
|
851
|
+
this.#c = options.colors;
|
|
852
|
+
this.#now = options.now ?? Date.now;
|
|
853
|
+
const { heartbeatMs } = options.output;
|
|
854
|
+
if (options.timers !== false && heartbeatMs > 0) {
|
|
855
|
+
this.#timer = setInterval(() => this.tick(), Math.min(heartbeatMs, 5e3));
|
|
856
|
+
this.#timer.unref();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
#write(text) {
|
|
860
|
+
this.#o.out.write(`${text.trimEnd()}\n`);
|
|
861
|
+
this.#lastWrite = this.#now();
|
|
862
|
+
}
|
|
863
|
+
#attempt(ref, startedAt) {
|
|
864
|
+
const n = ref.attempt?.n ?? 1;
|
|
865
|
+
const label = `[${this.#tags.label(ref.id, n)} ${attemptAxes(ref.attempt, this.#o.harness)}]`;
|
|
866
|
+
const ungated = ref.attempt?.gate === "none";
|
|
867
|
+
return {
|
|
868
|
+
key: ref.key,
|
|
869
|
+
id: ref.id,
|
|
870
|
+
label,
|
|
871
|
+
startedAt,
|
|
872
|
+
lines: [],
|
|
873
|
+
last: void 0,
|
|
874
|
+
ungated
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
#prefix(a) {
|
|
878
|
+
return this.#c.tag(a.label)(a.label);
|
|
879
|
+
}
|
|
880
|
+
#detail(a, line, prefixed) {
|
|
881
|
+
const body = line.debug ? ` ${line.text}` : ` ${this.#c.dim(offset(line.at - a.startedAt))} ${line.text}`;
|
|
882
|
+
return prefixed ? `${this.#prefix(a)}${body}` : body.slice(1);
|
|
883
|
+
}
|
|
884
|
+
get #streaming() {
|
|
885
|
+
return this.#o.output.logOrder === "stream" && this.#o.output.outputLogs === "full";
|
|
886
|
+
}
|
|
887
|
+
#add(a, line) {
|
|
888
|
+
a.lines.push(line);
|
|
889
|
+
if (this.#streaming) this.#write(this.#detail(a, line, true));
|
|
890
|
+
}
|
|
891
|
+
onEvent(e) {
|
|
892
|
+
const c = this.#c;
|
|
893
|
+
switch (e.type) {
|
|
894
|
+
case "run-start": {
|
|
895
|
+
this.#runStartedAt = e.at;
|
|
896
|
+
const files = `${e.files} eval file${e.files === 1 ? "" : "s"}`;
|
|
897
|
+
const order = `${this.#o.output.logOrder} logs`;
|
|
898
|
+
const setup = `harness ${e.harness} · sandbox ${e.sandbox} · concurrency ${e.concurrency}`;
|
|
899
|
+
this.#write(c.dim(`reeval ${e.version} · ${files} · ${setup} · ${order}`));
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
case "plan":
|
|
903
|
+
for (const [i, line] of e.lines.entries()) this.#write(line.startsWith("warning:") ? c.yellow(line) : i === 0 ? line : c.dim(line));
|
|
904
|
+
for (const w of e.sandboxWarnings ?? []) this.#sandboxWarning(w);
|
|
905
|
+
return;
|
|
906
|
+
case "collected":
|
|
907
|
+
this.#tags.addFile(e.evals);
|
|
908
|
+
for (const x of e.evals) if (!x.skipped) this.#queued.add(x.key);
|
|
909
|
+
return;
|
|
910
|
+
case "eval-start": {
|
|
911
|
+
const a = this.#attempt(e, e.at);
|
|
912
|
+
a.last = e;
|
|
913
|
+
this.#queued.delete(e.key);
|
|
914
|
+
this.#running.set(e.key, a);
|
|
915
|
+
this.#write(`${this.#prefix(a)} ${c.dim("▶")} ${e.id}`);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
case "eval-progress": {
|
|
919
|
+
const a = this.#running.get(e.key);
|
|
920
|
+
if (!a) return;
|
|
921
|
+
a.last = e;
|
|
922
|
+
const { progress } = e;
|
|
923
|
+
const paint = a.ungated ? {
|
|
924
|
+
...c,
|
|
925
|
+
red: c.dim
|
|
926
|
+
} : c;
|
|
927
|
+
const line = {
|
|
928
|
+
at: progress.at,
|
|
929
|
+
text: progressLine(paint, progress),
|
|
930
|
+
kind: progress.kind
|
|
931
|
+
};
|
|
932
|
+
const start = STARTS[progress.kind];
|
|
933
|
+
const open = start && a.lines.findLastIndex((l) => l.kind === start);
|
|
934
|
+
if (!this.#streaming && open !== void 0 && open >= 0) {
|
|
935
|
+
line.at = a.lines[open]?.at ?? line.at;
|
|
936
|
+
a.lines.splice(open, 1);
|
|
937
|
+
}
|
|
938
|
+
this.#add(a, line);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
case "debug": {
|
|
942
|
+
const a = this.#running.get(e.key);
|
|
943
|
+
if (!a) return;
|
|
944
|
+
for (const text of debugLines(c, e.line, this.#o.verbosity === 2)) this.#add(a, {
|
|
945
|
+
at: e.line.at,
|
|
946
|
+
text,
|
|
947
|
+
debug: true
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
case "log": {
|
|
952
|
+
const a = e.key ? this.#running.get(e.key) : void 0;
|
|
953
|
+
const label = e.stream === "stderr" ? c.yellow("stderr |") : c.dim("stdout |");
|
|
954
|
+
for (const text of e.text.replace(/\n$/, "").split("\n")) if (a) this.#add(a, {
|
|
955
|
+
at: e.at,
|
|
956
|
+
text: `${label} ${text}`
|
|
957
|
+
});
|
|
958
|
+
else this.#write(`${label} ${text}`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
case "eval-end":
|
|
962
|
+
this.#end(e.result, e.at);
|
|
963
|
+
return;
|
|
964
|
+
case "prepare":
|
|
965
|
+
for (const line of prepareLines(c, e, ERROR_LINES)) this.#write(line);
|
|
966
|
+
if (e.state === "failed" && this.#o.output.githubActions) {
|
|
967
|
+
const { target, failure } = e.outcome;
|
|
968
|
+
const what = target.kind === "workspace" ? "The" : "Sandbox";
|
|
969
|
+
const title = ghEscape(`${what} ${target.label} failed to prepare`, true);
|
|
970
|
+
this.#write(`::error title=${title}::${ghEscape(failure.message)}`);
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
case "notice": {
|
|
974
|
+
const paint = {
|
|
975
|
+
info: c.dim,
|
|
976
|
+
warn: c.yellow,
|
|
977
|
+
error: c.red
|
|
978
|
+
}[e.level];
|
|
979
|
+
this.#write(paint(e.text));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
case "run-end":
|
|
983
|
+
this.#write("");
|
|
984
|
+
for (const line of renderSummary(c, e, this.#o.attemptDir)) this.#write(line);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
#sandboxWarning(w) {
|
|
989
|
+
const c = this.#c;
|
|
990
|
+
const line = sandboxWarningLine(w);
|
|
991
|
+
this.#write(w.kind === "root" ? c.bold(c.red(line)) : c.yellow(line));
|
|
992
|
+
if (!this.#o.output.githubActions) return;
|
|
993
|
+
const file = w.file ? `file=${ghEscape(w.file, true)},` : "";
|
|
994
|
+
const title = w.kind === "root" ? "Sandbox runs as root" : "Sandbox has extra capabilities";
|
|
995
|
+
this.#write(`::warning ${file}title=${ghEscape(title, true)}::${ghEscape(w.message)}`);
|
|
996
|
+
}
|
|
997
|
+
#end(result, at) {
|
|
998
|
+
const c = this.#c;
|
|
999
|
+
const a = this.#running.get(result.key) ?? this.#attempt(result, at - result.durationMs);
|
|
1000
|
+
this.#running.delete(result.key);
|
|
1001
|
+
this.#queued.delete(result.key);
|
|
1002
|
+
const outcome = outcomeOf(result);
|
|
1003
|
+
const failed = outcome !== "pass" && outcome !== "skipped";
|
|
1004
|
+
const time = outcome === "skipped" ? "" : ` ${c.dim(formatDuration(result.durationMs))}`;
|
|
1005
|
+
const ungated = failed && result.attempt?.gate === "none" ? ` ${c.dim("(not gated)")}` : "";
|
|
1006
|
+
const missed = ungated !== "" && outcome === "fail";
|
|
1007
|
+
const head = `${this.#prefix(a)} ${missed ? MISS_MARK(c) : mark(c, outcome)} ${result.id}${time}${ungated}`;
|
|
1008
|
+
const { outputLogs, githubActions, logPrefix } = this.#o.output;
|
|
1009
|
+
const showDetail = !this.#streaming && a.lines.length > 0 && (outputLogs === "full" || outputLogs === "errors-only" && failed);
|
|
1010
|
+
const judged = result.meta?.errorKind === "judge" && (showDetail || this.#streaming);
|
|
1011
|
+
const errors = failed && outputLogs !== "none" && !judged ? missed ? failureLines(colors(false), result, { max: 1 }).filter((l) => l.trim()).slice(0, 1).map((l) => c.dim(l)) : failureLines(c, result, { max: ERROR_LINES }) : [];
|
|
1012
|
+
const lines = a.lines.filter((l) => l.kind !== "cleanup").sort((x, y) => x.at - y.at);
|
|
1013
|
+
const prefixed = logPrefix === "task";
|
|
1014
|
+
const group = githubActions && showDetail && (!failed || missed);
|
|
1015
|
+
this.#write(group ? `::group::${stripAnsi(head)}` : head);
|
|
1016
|
+
if (showDetail) for (const line of lines) this.#write(this.#detail(a, line, prefixed));
|
|
1017
|
+
for (const line of errors) this.#write(prefixed ? `${this.#prefix(a)} ${line}` : ` ${line}`);
|
|
1018
|
+
if (group) this.#write("::endgroup::");
|
|
1019
|
+
if (githubActions && failed && !ungated && !result.meta?.prepare) {
|
|
1020
|
+
const which = result.attempt?.label ? ` [${result.attempt.label}]` : "";
|
|
1021
|
+
const title = ghEscape(`${result.id}${which}`, true);
|
|
1022
|
+
const message = ghEscape(failureLines(colors(false), result, { max: ERROR_LINES }).join("\n"));
|
|
1023
|
+
const at = locate(result);
|
|
1024
|
+
const where = at ? `,line=${at.line},col=${at.col}` : "";
|
|
1025
|
+
this.#write(`::error file=${ghEscape(result.file, true)}${where},title=${title}::${message}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/** Prints the heartbeat if nothing was written for `heartbeatMs` and evals are running. */
|
|
1029
|
+
tick(now = this.#now()) {
|
|
1030
|
+
const { heartbeatMs } = this.#o.output;
|
|
1031
|
+
if (heartbeatMs <= 0 || this.#running.size === 0) return;
|
|
1032
|
+
if (now - this.#lastWrite < heartbeatMs) return;
|
|
1033
|
+
const c = this.#c;
|
|
1034
|
+
const running = [...this.#running.values()].map((a) => {
|
|
1035
|
+
const progress = a.last?.type === "eval-progress" ? a.last.progress : void 0;
|
|
1036
|
+
return `${this.#prefix(a)} ${phaseOf(progress)} ${c.dim(formatDuration(now - a.startedAt))}`;
|
|
1037
|
+
});
|
|
1038
|
+
const queued = this.#queued.size ? `, ${this.#queued.size} queued` : "";
|
|
1039
|
+
const elapsed = formatDuration(now - this.#runStartedAt);
|
|
1040
|
+
this.#write(`${c.dim(`… ${this.#running.size} running${queued} after ${elapsed}:`)} ${running.join(c.dim(", "))}`);
|
|
1041
|
+
}
|
|
1042
|
+
close() {
|
|
1043
|
+
clearInterval(this.#timer);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region src/ui/mode.ts
|
|
1048
|
+
const set = (env, name) => {
|
|
1049
|
+
const value = env[name];
|
|
1050
|
+
return !!value && value !== "false" && value !== "0";
|
|
1051
|
+
};
|
|
1052
|
+
const CI_VARS = [
|
|
1053
|
+
"BUILD_ID",
|
|
1054
|
+
"BUILD_NUMBER",
|
|
1055
|
+
"CI",
|
|
1056
|
+
"CI_APP_ID",
|
|
1057
|
+
"CI_BUILD_ID",
|
|
1058
|
+
"CI_BUILD_NUMBER",
|
|
1059
|
+
"CI_NAME",
|
|
1060
|
+
"CONTINUOUS_INTEGRATION",
|
|
1061
|
+
"RUN_ID",
|
|
1062
|
+
"TEAMCITY_VERSION",
|
|
1063
|
+
"GITHUB_ACTIONS"
|
|
1064
|
+
];
|
|
1065
|
+
const isCI = (env) => CI_VARS.some((name) => set(env, name));
|
|
1066
|
+
function pick(name, value, allowed) {
|
|
1067
|
+
if (value === void 0 || value === "") return void 0;
|
|
1068
|
+
if (value === "auto" || allowed.includes(value)) return value;
|
|
1069
|
+
throw new UsageError(`${name} must be one of ${[...allowed, "auto"].join(", ")}`);
|
|
1070
|
+
}
|
|
1071
|
+
function parseHeartbeat(value) {
|
|
1072
|
+
if (!value) return 6e4;
|
|
1073
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(value);
|
|
1074
|
+
if (!match) throw new UsageError(`REEVAL_HEARTBEAT must be a duration like 30s or 0`);
|
|
1075
|
+
const unit = {
|
|
1076
|
+
ms: 1,
|
|
1077
|
+
s: 1e3,
|
|
1078
|
+
m: 6e4
|
|
1079
|
+
}[match[2] ?? "s"] ?? 1e3;
|
|
1080
|
+
return Number(match[1]) * unit;
|
|
1081
|
+
}
|
|
1082
|
+
function resolveOutput(input) {
|
|
1083
|
+
const { flags, env } = input;
|
|
1084
|
+
const ci = isCI(env);
|
|
1085
|
+
const githubActions = set(env, "GITHUB_ACTIONS");
|
|
1086
|
+
const ui = pick("--ui", flags.ui ?? env.REEVAL_UI, [
|
|
1087
|
+
"live",
|
|
1088
|
+
"plain",
|
|
1089
|
+
"agent"
|
|
1090
|
+
]);
|
|
1091
|
+
const requested = ui && ui !== "auto" ? ui : input.agent ? "agent" : ci || !input.isTTY ? "plain" : "live";
|
|
1092
|
+
const mode = requested === "live" ? "plain" : requested;
|
|
1093
|
+
const order = pick("--log-order", flags.logOrder ?? env.REEVAL_LOG_ORDER, ["stream", "grouped"]);
|
|
1094
|
+
const logOrder = order && order !== "auto" ? order : ci ? "grouped" : "stream";
|
|
1095
|
+
const prefix = pick("--log-prefix", flags.logPrefix ?? env.REEVAL_LOG_PREFIX, ["task", "none"]);
|
|
1096
|
+
const logPrefix = prefix && prefix !== "auto" ? prefix : githubActions && logOrder === "grouped" ? "none" : "task";
|
|
1097
|
+
const logs = pick("--output-logs", flags.outputLogs ?? env.REEVAL_OUTPUT_LOGS, [
|
|
1098
|
+
"full",
|
|
1099
|
+
"errors-only",
|
|
1100
|
+
"none"
|
|
1101
|
+
]);
|
|
1102
|
+
return {
|
|
1103
|
+
requested,
|
|
1104
|
+
mode,
|
|
1105
|
+
logOrder,
|
|
1106
|
+
logPrefix,
|
|
1107
|
+
outputLogs: logs && logs !== "auto" ? logs : "full",
|
|
1108
|
+
color: env.NO_COLOR ? false : env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" ? set(env, "FORCE_COLOR") : mode === "agent" ? false : input.isTTY || githubActions,
|
|
1109
|
+
githubActions,
|
|
1110
|
+
heartbeatMs: mode === "agent" ? 0 : parseHeartbeat(env.REEVAL_HEARTBEAT)
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/ui/index.ts
|
|
1115
|
+
function createRenderer(options) {
|
|
1116
|
+
const out = { write: (text) => options.out.write(redactSecrets(text)) };
|
|
1117
|
+
if (options.output.mode === "agent") return new AgentRenderer({
|
|
1118
|
+
...options,
|
|
1119
|
+
out
|
|
1120
|
+
});
|
|
1121
|
+
return new PlainRenderer({
|
|
1122
|
+
...options,
|
|
1123
|
+
out,
|
|
1124
|
+
colors: colors(options.output.color)
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
//#endregion
|
|
1128
|
+
//#region src/cli.ts
|
|
1129
|
+
const HISTORY_COMMANDS = {
|
|
1130
|
+
record: 1,
|
|
1131
|
+
history: 1,
|
|
1132
|
+
show: 1,
|
|
1133
|
+
compare: 1,
|
|
1134
|
+
checkout: 1,
|
|
1135
|
+
query: 1,
|
|
1136
|
+
analyze: 1
|
|
1137
|
+
};
|
|
1138
|
+
const EXPECTED_ERRORS = [
|
|
1139
|
+
"CommandError",
|
|
1140
|
+
"RefError",
|
|
1141
|
+
"HistoryError",
|
|
1142
|
+
"StudioBuildError"
|
|
1143
|
+
];
|
|
1144
|
+
const EXIT = {
|
|
1145
|
+
ok: 0,
|
|
1146
|
+
failed: 1,
|
|
1147
|
+
usage: 2,
|
|
1148
|
+
infra: 3,
|
|
1149
|
+
interrupted: 130,
|
|
1150
|
+
terminated: 143
|
|
1151
|
+
};
|
|
1152
|
+
const version = PACKAGE_VERSION;
|
|
1153
|
+
const HELP = helpText(version);
|
|
1154
|
+
const { green, red, dim } = colors(!!process.stdout.isTTY && !process.env.NO_COLOR);
|
|
1155
|
+
const startedAt = Date.now();
|
|
1156
|
+
const argv = process.argv.slice(2);
|
|
1157
|
+
/** What the invocation log needs from the command as it goes. */
|
|
1158
|
+
const invocation = {
|
|
1159
|
+
runId: null,
|
|
1160
|
+
unhandledErrors: []
|
|
1161
|
+
};
|
|
1162
|
+
/** Set by the first Ctrl-C or SIGTERM of `reeval run`. */
|
|
1163
|
+
let interrupted = false;
|
|
1164
|
+
/** The last signal that cancelled the run: 130 after SIGINT, 143 after SIGTERM. */
|
|
1165
|
+
let stopCode = EXIT.interrupted;
|
|
1166
|
+
async function run(argv) {
|
|
1167
|
+
const { values, positionals } = parseCli(argv);
|
|
1168
|
+
invocation.flags = values;
|
|
1169
|
+
if (values.version) {
|
|
1170
|
+
console.log(version);
|
|
1171
|
+
return EXIT.ok;
|
|
1172
|
+
}
|
|
1173
|
+
const [command, ...filters] = positionals;
|
|
1174
|
+
if (values.help || !command) {
|
|
1175
|
+
console.log(HELP);
|
|
1176
|
+
return values.help ? EXIT.ok : EXIT.usage;
|
|
1177
|
+
}
|
|
1178
|
+
if (command === "init") {
|
|
1179
|
+
const { initCommand } = await import("./init-z2ZPn9Tj.js");
|
|
1180
|
+
return initCommand(values, filters, {
|
|
1181
|
+
out: (s) => console.log(s),
|
|
1182
|
+
input: process.stdin,
|
|
1183
|
+
output: process.stdout,
|
|
1184
|
+
cwd: process.cwd()
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (command === "doctor") {
|
|
1188
|
+
const { doctorCommand } = await import("./doctor-CxlH1pDM.js");
|
|
1189
|
+
return doctorCommand(values, {
|
|
1190
|
+
out: (s) => console.log(s),
|
|
1191
|
+
color: !!process.stdout.isTTY && !process.env.NO_COLOR,
|
|
1192
|
+
confirm: process.stdin.isTTY && process.stdout.isTTY ? confirm : void 0
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
if (command === "report") {
|
|
1196
|
+
const { reportCommand } = await import("./command-D-jnQWVG.js");
|
|
1197
|
+
return reportCommand(values, filters, {
|
|
1198
|
+
out: (s) => console.log(s),
|
|
1199
|
+
err: (s) => console.error(dim(s)),
|
|
1200
|
+
interactive: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
1201
|
+
confirm: process.stdin.isTTY && process.stdout.isTTY ? confirm : void 0
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
const configPath = configPathFor(values.config);
|
|
1205
|
+
if (command === "image") {
|
|
1206
|
+
if (filters[0] !== "build") throw new UsageError(`Unknown command "image ${filters[0] ?? ""}". Did you mean reeval image build?`);
|
|
1207
|
+
return imageBuildCommand(values, configPath, filters.slice(1));
|
|
1208
|
+
}
|
|
1209
|
+
if (command === "studio") {
|
|
1210
|
+
const config = await loadConfig(configPath, "studio");
|
|
1211
|
+
const { studioCommand } = await import("./command-BSJzHMtP.js");
|
|
1212
|
+
return studioCommand({
|
|
1213
|
+
config,
|
|
1214
|
+
flags: values,
|
|
1215
|
+
version,
|
|
1216
|
+
build: filters[0] === "build"
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
if (command === "mcp") {
|
|
1220
|
+
if (!values.http) console.log = console.info = console.debug = console.error;
|
|
1221
|
+
const config = await loadConfig(configPath, "mcp");
|
|
1222
|
+
const { mcpCommand } = await import("./command-DQAvbG7d.js");
|
|
1223
|
+
return mcpCommand({
|
|
1224
|
+
config,
|
|
1225
|
+
flags: values,
|
|
1226
|
+
version
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if (command in HISTORY_COMMANDS) {
|
|
1230
|
+
const config = await loadConfig(configPath, command);
|
|
1231
|
+
const commands = await import("./commands-A4udfyKZ.js");
|
|
1232
|
+
const ctx = {
|
|
1233
|
+
config,
|
|
1234
|
+
flags: values,
|
|
1235
|
+
out: (s) => console.log(s)
|
|
1236
|
+
};
|
|
1237
|
+
switch (command) {
|
|
1238
|
+
case "record": return commands.recordCommand(ctx, filters);
|
|
1239
|
+
case "history": return commands.historyCommand(ctx, filters);
|
|
1240
|
+
case "show": return commands.showCommand(ctx, filters);
|
|
1241
|
+
case "compare": return commands.compareCommand(ctx, filters);
|
|
1242
|
+
case "checkout": return commands.checkoutCommand(ctx, filters);
|
|
1243
|
+
case "query": {
|
|
1244
|
+
const { queryCommand } = await import("./query-DR4m6CXf.js");
|
|
1245
|
+
return queryCommand({
|
|
1246
|
+
...ctx,
|
|
1247
|
+
err: (s) => console.error(dim(s))
|
|
1248
|
+
}, filters);
|
|
1249
|
+
}
|
|
1250
|
+
case "analyze": {
|
|
1251
|
+
loadEnvFiles(configPath);
|
|
1252
|
+
const { analyzeCommand } = await import("./command-CEm8PZ1N.js");
|
|
1253
|
+
return analyzeCommand({
|
|
1254
|
+
...ctx,
|
|
1255
|
+
err: (s) => console.error(dim(s))
|
|
1256
|
+
}, filters);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (command === "clean") return cleanCommand(values, configPath);
|
|
1261
|
+
if (command !== "run") throw new UsageError(`Unknown command "${command}". Run reeval --help.`);
|
|
1262
|
+
return runCommand(values, configPath, filters);
|
|
1263
|
+
}
|
|
1264
|
+
/** Asks a yes/no question on the terminal; anything but y or yes is no. */
|
|
1265
|
+
async function confirm(question) {
|
|
1266
|
+
const { createInterface } = await import("node:readline/promises");
|
|
1267
|
+
const rl = createInterface({
|
|
1268
|
+
input: process.stdin,
|
|
1269
|
+
output: process.stdout
|
|
1270
|
+
});
|
|
1271
|
+
try {
|
|
1272
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
1273
|
+
} finally {
|
|
1274
|
+
rl.close();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/** `reeval clean`: removes local debug data, all of it or all but the newest `--keep` runs. */
|
|
1278
|
+
async function cleanCommand(flags, configPath) {
|
|
1279
|
+
const config = await loadConfig(configPath, "clean");
|
|
1280
|
+
let keep = 0;
|
|
1281
|
+
if (flags.keep !== void 0) {
|
|
1282
|
+
keep = Number(flags.keep);
|
|
1283
|
+
if (!(Number.isInteger(keep) && keep >= 0)) throw new UsageError(`--keep must be a whole number (got ${flags.keep})`);
|
|
1284
|
+
}
|
|
1285
|
+
const { debugRoot, pruneDebug } = await import("./files-BvHCFWgD.js");
|
|
1286
|
+
const { formatBytes } = await import("./debug-SEmW4w8f.js");
|
|
1287
|
+
const { runs, bytes } = await pruneDebug(config.root, keep);
|
|
1288
|
+
const where = fromCwd(debugRoot(config.root)) || ".";
|
|
1289
|
+
console.log(runs.length ? `Removed debug data for ${runs.length} run${runs.length === 1 ? "" : "s"} (${formatBytes(bytes)}) from ${where}` : `No debug data to remove in ${where}`);
|
|
1290
|
+
if (flags.images) {
|
|
1291
|
+
const { pruneImages } = await import("./src-N7E_mfcZ.js");
|
|
1292
|
+
const { requireDocker } = await import("./providers-BUOxitcT.js");
|
|
1293
|
+
await requireDocker();
|
|
1294
|
+
const removed = await pruneImages();
|
|
1295
|
+
console.log(removed ? `Removed ${removed} image${removed === 1 ? "" : "s"} reeval built that nothing uses` : "No leftover images to remove");
|
|
1296
|
+
}
|
|
1297
|
+
return EXIT.ok;
|
|
1298
|
+
}
|
|
1299
|
+
/** `-v` is 1 and `-vv` (or `--trace`) is 2. */
|
|
1300
|
+
function verbosityOf(flags) {
|
|
1301
|
+
if (flags.trace) return 2;
|
|
1302
|
+
return Math.min(2, flags.verbose?.length ?? 0);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* `reeval image build`: collects the evals (as `--dry-run` does) and builds every image their Docker
|
|
1306
|
+
* sandboxes need; with `--dry-run`, lists them instead. Without a config, just reeval's image.
|
|
1307
|
+
*/
|
|
1308
|
+
async function imageBuildCommand(flags, configPath, filters) {
|
|
1309
|
+
const { buildImages, imagePlanLines, imageTargets } = await import("./images-Cc1-coFc.js");
|
|
1310
|
+
const { docker } = await import("./sandbox.js");
|
|
1311
|
+
let root = process.cwd();
|
|
1312
|
+
const { fakeHarness } = await import("./harness.js");
|
|
1313
|
+
let uses = [{
|
|
1314
|
+
id: "",
|
|
1315
|
+
harness: fakeHarness(),
|
|
1316
|
+
sandbox: docker()
|
|
1317
|
+
}];
|
|
1318
|
+
if (configPath) {
|
|
1319
|
+
const { runId, stagingDir, config, runs, tags, concurrency, profile } = await startRun(await prepareRun({
|
|
1320
|
+
...flags,
|
|
1321
|
+
"dry-run": true
|
|
1322
|
+
}, configPath));
|
|
1323
|
+
root = config.root;
|
|
1324
|
+
const { runEngine } = await import("./node-kG64XxdE.js");
|
|
1325
|
+
let planned = [];
|
|
1326
|
+
await runEngine({
|
|
1327
|
+
run: JSON.parse(JSON.stringify({
|
|
1328
|
+
runId,
|
|
1329
|
+
stagingDir,
|
|
1330
|
+
config,
|
|
1331
|
+
runs,
|
|
1332
|
+
tags,
|
|
1333
|
+
concurrency,
|
|
1334
|
+
profile
|
|
1335
|
+
})),
|
|
1336
|
+
filters,
|
|
1337
|
+
onPlan: async (collected) => {
|
|
1338
|
+
if (collected.unmatched.length) throw unmatchedFilter(config, collected.unmatched);
|
|
1339
|
+
planned = collected.planned.filter((p) => !p.attempt.plan.skip);
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
uses = planned.length ? planned.map((p) => ({
|
|
1344
|
+
id: p.id,
|
|
1345
|
+
harness: p.attempt.harness,
|
|
1346
|
+
sandbox: p.attempt.sandbox
|
|
1347
|
+
})) : config.harness.map((harness) => ({
|
|
1348
|
+
id: "",
|
|
1349
|
+
harness,
|
|
1350
|
+
sandbox: config.sandbox
|
|
1351
|
+
}));
|
|
1352
|
+
}
|
|
1353
|
+
const targets = imageTargets(uses);
|
|
1354
|
+
if (targets.length === 0) {
|
|
1355
|
+
console.log("No eval runs in a Docker sandbox, so there are no images to build.");
|
|
1356
|
+
return EXIT.ok;
|
|
1357
|
+
}
|
|
1358
|
+
if (flags["dry-run"]) {
|
|
1359
|
+
for (const line of await imagePlanLines(targets, root)) console.log(line);
|
|
1360
|
+
return EXIT.ok;
|
|
1361
|
+
}
|
|
1362
|
+
const { PrepareError } = await import("./provider-BDbNOQhQ.js");
|
|
1363
|
+
let built;
|
|
1364
|
+
try {
|
|
1365
|
+
built = await buildImages(targets, root, {
|
|
1366
|
+
log: (line) => console.log(dim(line)),
|
|
1367
|
+
force: !!flags.force
|
|
1368
|
+
});
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
if (error instanceof PrepareError && error.failure.usage) throw new UsageError(error.message);
|
|
1371
|
+
throw error;
|
|
1372
|
+
}
|
|
1373
|
+
for (const b of built) {
|
|
1374
|
+
if (b.ok) {
|
|
1375
|
+
const id = b.image.id.replace(/^sha256:/, "").slice(0, 12);
|
|
1376
|
+
console.log(`${green("✓")} ${green(`${b.id}: ${b.image.ref}`)}${dim(` ${id}`)}`);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const uses = b.evals ? ` (${b.evals} eval${b.evals === 1 ? "" : "s"})` : "";
|
|
1380
|
+
const [headline = "", ...rest] = b.failure.message.split("\n");
|
|
1381
|
+
console.log(`${red("✗")} ${red(`${b.id}${uses}: ${headline}`)}`);
|
|
1382
|
+
for (const line of rest) console.log(` ${line}`);
|
|
1383
|
+
}
|
|
1384
|
+
const failed = built.filter((b) => !b.ok);
|
|
1385
|
+
const n = built.length;
|
|
1386
|
+
console.log(failed.length ? red(`${failed.length} of ${n} image${n === 1 ? "" : "s"} failed to build`) : dim(`${n} image${n === 1 ? "" : "s"} ready`));
|
|
1387
|
+
if (!failed.length) return EXIT.ok;
|
|
1388
|
+
return failed.every((b) => !b.ok && b.failure.usage) ? EXIT.usage : EXIT.infra;
|
|
1389
|
+
}
|
|
1390
|
+
/** `reeval run`: check, plan, run the engine, then write the run and pick the exit code. */
|
|
1391
|
+
async function runCommand(flags, configPath, filters) {
|
|
1392
|
+
const setup = await prepareRun(flags, configPath);
|
|
1393
|
+
const ctx = await startRun(setup);
|
|
1394
|
+
invocation.runId = ctx.runId;
|
|
1395
|
+
const { engineVersions } = await import("./node-kG64XxdE.js");
|
|
1396
|
+
const renderer = createRenderer({
|
|
1397
|
+
out: setup.json ? { write: () => true } : process.stdout,
|
|
1398
|
+
output: setup.output,
|
|
1399
|
+
verbosity: ctx.verbosity ?? 0,
|
|
1400
|
+
harness: setup.config.harness[0]?.kind ?? "agent",
|
|
1401
|
+
attemptDir: (id, cell, n) => fromCwd(join(ctx.scratch, ctx.runId, evalSlug(id), evalSlug(cell), String(n)))
|
|
1402
|
+
});
|
|
1403
|
+
let executed;
|
|
1404
|
+
try {
|
|
1405
|
+
executed = await executeRun(ctx, setup, filters, renderer);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
renderer.close();
|
|
1408
|
+
await rm(ctx.stagingDir, {
|
|
1409
|
+
recursive: true,
|
|
1410
|
+
force: true
|
|
1411
|
+
});
|
|
1412
|
+
throw error;
|
|
1413
|
+
}
|
|
1414
|
+
const { engineRun, plan, sandboxVersions, budgetReached, live } = executed;
|
|
1415
|
+
invocation.unhandledErrors = engineRun.unhandledErrors;
|
|
1416
|
+
if ((interrupted || engineRun.cancelled) && plan?.sandboxes.some((s) => s.kind === "docker")) {
|
|
1417
|
+
const { removeRunContainers } = await import("./docker-DnWAVdEm.js");
|
|
1418
|
+
await removeRunContainers(ctx.runId);
|
|
1419
|
+
}
|
|
1420
|
+
if (engineRun.stopped || !plan) {
|
|
1421
|
+
live?.remove();
|
|
1422
|
+
renderer.close();
|
|
1423
|
+
return EXIT.ok;
|
|
1424
|
+
}
|
|
1425
|
+
const { lockfiles } = await import("./state-Cyw3QTsN.js");
|
|
1426
|
+
const { writeScratchRun } = await import("./results-CeXb8Wde.js");
|
|
1427
|
+
const run = await writeScratchRun({
|
|
1428
|
+
ctx,
|
|
1429
|
+
author: ctx.author,
|
|
1430
|
+
engine: engineRun,
|
|
1431
|
+
startedAt: ctx.startedAt,
|
|
1432
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
1433
|
+
interrupted: interrupted || engineRun.cancelled,
|
|
1434
|
+
budgetReached: budgetReached(),
|
|
1435
|
+
plan,
|
|
1436
|
+
es: version,
|
|
1437
|
+
enginePackages: engineVersions,
|
|
1438
|
+
sandbox: sandboxVersions,
|
|
1439
|
+
worktreePatch: ctx.worktreePatch,
|
|
1440
|
+
lockfiles: await lockfiles(ctx.config.root),
|
|
1441
|
+
git: ctx.git,
|
|
1442
|
+
runner: runnerInfo()
|
|
1443
|
+
}, () => live?.stop());
|
|
1444
|
+
const { record } = run;
|
|
1445
|
+
const { pruneDebug } = await import("./files-BvHCFWgD.js");
|
|
1446
|
+
await pruneDebug(ctx.config.root, ctx.config.debug.keep).catch(() => {});
|
|
1447
|
+
const summary = run.artifactDir ? join(run.artifactDir, "summary.md") : run.recordPath;
|
|
1448
|
+
renderer.onEvent({
|
|
1449
|
+
type: "run-end",
|
|
1450
|
+
at: Date.now(),
|
|
1451
|
+
record,
|
|
1452
|
+
summaryPath: fromCwd(summary),
|
|
1453
|
+
results: engineRun.evals,
|
|
1454
|
+
unhandledErrors: engineRun.unhandledErrors
|
|
1455
|
+
});
|
|
1456
|
+
renderer.close();
|
|
1457
|
+
const stepSummary = process.env.GITHUB_STEP_SUMMARY;
|
|
1458
|
+
if (stepSummary && run.artifactDir) await appendFile(stepSummary, await readFile(summary)).catch(() => {});
|
|
1459
|
+
const { config } = ctx;
|
|
1460
|
+
const mode = setup.record;
|
|
1461
|
+
if ((flags.record || mode === "always" || mode === "ci" && inCI()) && record.status !== "interrupted") {
|
|
1462
|
+
const { record: recordCmd } = await import("./commands-A4udfyKZ.js");
|
|
1463
|
+
const allow = record.status === "budget" ? { "allow-incomplete": true } : {};
|
|
1464
|
+
console.log(await recordCmd(config, record.runId, {
|
|
1465
|
+
...flags,
|
|
1466
|
+
...allow
|
|
1467
|
+
}));
|
|
1468
|
+
}
|
|
1469
|
+
if (interrupted || engineRun.cancelled) return stopCode;
|
|
1470
|
+
if (engineRun.unhandledErrors.length > 0) return EXIT.failed;
|
|
1471
|
+
const outcome = runOutcome(record);
|
|
1472
|
+
return outcome === "fail" ? EXIT.failed : outcome === "infra_error" ? EXIT.infra : EXIT.ok;
|
|
1473
|
+
}
|
|
1474
|
+
/** Everything that can fail before the evals are collected: output mode, config, profile, flags. */
|
|
1475
|
+
async function prepareRun(flags, configPath) {
|
|
1476
|
+
const output = resolveOutput({
|
|
1477
|
+
flags: {
|
|
1478
|
+
ui: flags.ui,
|
|
1479
|
+
logOrder: flags["log-order"],
|
|
1480
|
+
logPrefix: flags["log-prefix"],
|
|
1481
|
+
outputLogs: flags["output-logs"]
|
|
1482
|
+
},
|
|
1483
|
+
env: process.env,
|
|
1484
|
+
isTTY: !!process.stdout.isTTY,
|
|
1485
|
+
agent: detectAgent().name
|
|
1486
|
+
});
|
|
1487
|
+
if (flags.json && !flags["dry-run"]) throw new UsageError("reeval run --json only prints the plan: add --dry-run. A run writes its record to .reeval/runs; read it with reeval show --json.");
|
|
1488
|
+
loadEnvFiles(configPath);
|
|
1489
|
+
const named = flags.profile ?? (process.env.REEVAL_PROFILE || void 0);
|
|
1490
|
+
const config = await loadConfig(configPath, "run", named);
|
|
1491
|
+
const profile = selectProfile(config, flags.profile);
|
|
1492
|
+
let runs;
|
|
1493
|
+
if (flags.runs !== void 0) {
|
|
1494
|
+
runs = Number(flags.runs);
|
|
1495
|
+
if (!(Number.isInteger(runs) && runs >= 1)) throw new UsageError(`--runs must be a whole number of at least 1 (got ${flags.runs})`);
|
|
1496
|
+
}
|
|
1497
|
+
const sandboxes = [config.sandbox, ...profile?.defaults.sandbox ? [profile.defaults.sandbox] : []];
|
|
1498
|
+
const concurrency = await resolveConcurrency(config, flags.concurrency, async () => {
|
|
1499
|
+
if (!sandboxes.some((s) => s.kind === "docker")) return hostCapacity();
|
|
1500
|
+
const { dockerCapacity } = await import("./src-N7E_mfcZ.js");
|
|
1501
|
+
return await dockerCapacity() ?? hostCapacity();
|
|
1502
|
+
});
|
|
1503
|
+
return {
|
|
1504
|
+
output,
|
|
1505
|
+
config,
|
|
1506
|
+
profile,
|
|
1507
|
+
runs,
|
|
1508
|
+
tags: [...flags.tag ?? [], ...profile?.tags ?? []],
|
|
1509
|
+
concurrency,
|
|
1510
|
+
budget: profile?.budget ?? config.budget,
|
|
1511
|
+
record: profile?.record ?? config.record,
|
|
1512
|
+
dryRun: !!flags["dry-run"],
|
|
1513
|
+
json: !!flags.json,
|
|
1514
|
+
force: !!flags.force,
|
|
1515
|
+
verbosity: verbosityOf(flags),
|
|
1516
|
+
configFile: configPath && configLabel(configPath),
|
|
1517
|
+
/** Named in the plan line when the project root isn't the current directory. */
|
|
1518
|
+
configElsewhere: !!configPath && dirname(configPath) !== process.cwd()
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
/** The run's id and the state it starts from: commit, uncommitted changes, author. */
|
|
1522
|
+
async function startRun({ config, profile, runs, tags, concurrency, verbosity, configFile }) {
|
|
1523
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
1524
|
+
const git = await gitInfo(config.root, [config.history.dir]);
|
|
1525
|
+
const { worktreePatch } = await import("./state-Cyw3QTsN.js");
|
|
1526
|
+
const patch = git?.dirty ? await worktreePatch(config.root, [config.history.dir]) : null;
|
|
1527
|
+
const who = await author(config.root);
|
|
1528
|
+
const { stagingDir, sweepStaging } = await import("./results-CeXb8Wde.js");
|
|
1529
|
+
const { scratchDir } = await import("./files--6CEUXm7.js");
|
|
1530
|
+
const scratch = scratchDir(config.root);
|
|
1531
|
+
const stamps = new Set([scratch, config.history.dir].flatMap((dir) => existsSync(dir) ? readdirSync(dir).map((name) => name.slice(0, 16)) : []));
|
|
1532
|
+
let runId = newRunId(startedAt, git?.sha ?? null, who);
|
|
1533
|
+
for (let n = 1; stamps.has(runId.slice(0, 16)); n++) runId = newRunId(new Date(startedAt.getTime() + n * 1e3), git?.sha ?? null, who);
|
|
1534
|
+
await sweepStaging(config.root);
|
|
1535
|
+
return {
|
|
1536
|
+
runId,
|
|
1537
|
+
stagingDir: stagingDir(config.root, runId),
|
|
1538
|
+
config,
|
|
1539
|
+
runs,
|
|
1540
|
+
tags,
|
|
1541
|
+
concurrency,
|
|
1542
|
+
profile,
|
|
1543
|
+
verbosity,
|
|
1544
|
+
configFile,
|
|
1545
|
+
startedAt,
|
|
1546
|
+
git,
|
|
1547
|
+
worktreePatch: patch,
|
|
1548
|
+
author: who,
|
|
1549
|
+
scratch
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function noMatch(config, filters, tags) {
|
|
1553
|
+
const narrowed = [filters.length ? `filter "${filters.join(" ")}"` : "", tags.length ? `tags ${tags.join(", ")}` : ""].filter(Boolean);
|
|
1554
|
+
return new UsageError(`No evals matched ${config.include.join(", ")}${narrowed.length ? ` with ${narrowed.join(" and ")}` : ""}`);
|
|
1555
|
+
}
|
|
1556
|
+
function unmatchedFilter(config, parts) {
|
|
1557
|
+
const which = parts.map((p) => `"${p}"`).join(", ");
|
|
1558
|
+
return new UsageError(`filter ${which} ${parts.length === 1 ? "matches" : "match"} no eval file, eval name or cell in ${config.include.join(", ")}`);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Collects the evals and prints the plan; stops there for `--dry-run`, or on a budget, credential
|
|
1562
|
+
* or Docker problem (exit 2). Then runs the engine with live output and Ctrl-C handling.
|
|
1563
|
+
*/
|
|
1564
|
+
async function executeRun(ctx, setup, filters, renderer) {
|
|
1565
|
+
const { runId, stagingDir, config, runs, tags, concurrency, profile, verbosity, configFile } = ctx;
|
|
1566
|
+
const { runEngine } = await import("./node-kG64XxdE.js");
|
|
1567
|
+
const { buildPlan, checkAttemptBudget, planDocument, planLine, planTable } = await import("./plan-gwk6dJ8c.js");
|
|
1568
|
+
let plan;
|
|
1569
|
+
let sandboxVersions = { images: [] };
|
|
1570
|
+
let preflightStop;
|
|
1571
|
+
let reached = false;
|
|
1572
|
+
let budgetTimer;
|
|
1573
|
+
let toRun = [];
|
|
1574
|
+
let live;
|
|
1575
|
+
const onPlan = async ({ planned, excluded, matched, unmatched }) => {
|
|
1576
|
+
if (unmatched.length) throw unmatchedFilter(config, unmatched);
|
|
1577
|
+
if (matched === 0) throw noMatch(config, filters, tags);
|
|
1578
|
+
toRun = planned;
|
|
1579
|
+
for (const p of planned) for (const name of p.attempt.secretVars) registerSecret(process.env[name]);
|
|
1580
|
+
plan = await buildPlan({
|
|
1581
|
+
planned,
|
|
1582
|
+
profile,
|
|
1583
|
+
budget: setup.budget,
|
|
1584
|
+
concurrency,
|
|
1585
|
+
root: config.root,
|
|
1586
|
+
historyDir: config.history.dir
|
|
1587
|
+
});
|
|
1588
|
+
const { imageStages, imageTargets } = await import("./images-Cc1-coFc.js");
|
|
1589
|
+
const targets = imageTargets(planned.filter((p) => !p.attempt.plan.skip).map((p) => ({
|
|
1590
|
+
id: p.id,
|
|
1591
|
+
harness: p.attempt.harness,
|
|
1592
|
+
sandbox: p.attempt.sandbox
|
|
1593
|
+
})));
|
|
1594
|
+
if (setup.json) {
|
|
1595
|
+
const images = await Promise.all(targets.map(async (t) => ({
|
|
1596
|
+
id: t.id,
|
|
1597
|
+
harness: t.harness,
|
|
1598
|
+
evals: t.evals,
|
|
1599
|
+
stages: await imageStages(t, config.root)
|
|
1600
|
+
})));
|
|
1601
|
+
const doc = planDocument(plan, planned, excluded, images);
|
|
1602
|
+
process.stdout.write(`${JSON.stringify(doc, null, 2)}\n`);
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
const lines = [planLine(plan, setup.configElsewhere ? configFile : void 0)];
|
|
1606
|
+
if (setup.dryRun) {
|
|
1607
|
+
const todos = excluded.filter((e) => e.mode === "todo").map((e) => e.id);
|
|
1608
|
+
lines.push(...planTable(plan, todos));
|
|
1609
|
+
const { imageStatusLines } = await import("./images-Cc1-coFc.js");
|
|
1610
|
+
const { dockerAvailable } = await import("./src-N7E_mfcZ.js");
|
|
1611
|
+
if (targets.length && await dockerAvailable()) lines.push(...await imageStatusLines(targets, config.root));
|
|
1612
|
+
else if (targets.length) lines.push("images not checked: Docker is not running");
|
|
1613
|
+
}
|
|
1614
|
+
lines.push(...plan.warnings.map((w) => `warning: ${w}`));
|
|
1615
|
+
lines.push(...plan.hints.map((h) => `hint: ${h}`));
|
|
1616
|
+
renderer.onEvent({
|
|
1617
|
+
type: "plan",
|
|
1618
|
+
at: Date.now(),
|
|
1619
|
+
lines,
|
|
1620
|
+
sandboxWarnings: plan.sandboxWarnings
|
|
1621
|
+
});
|
|
1622
|
+
if (setup.dryRun) return false;
|
|
1623
|
+
checkAttemptBudget(plan, setup.force, config.file);
|
|
1624
|
+
const missing = missingCredentials(config, process.env, {
|
|
1625
|
+
harnesses: plan.harnessDescriptors,
|
|
1626
|
+
profile
|
|
1627
|
+
});
|
|
1628
|
+
if (missing.length) throw new UsageError(`Missing credentials: ${missing.join(", ")}. Set them in the environment or .env.local.`);
|
|
1629
|
+
const malformed = malformedCredentials(config, process.env, { harnesses: plan.harnessDescriptors });
|
|
1630
|
+
if (malformed.length) throw new UsageError(`Credentials with whitespace or a line break in them: ${malformed.join(", ")}. A key copied from a wrapped terminal line picks these up; set it again as one line.`);
|
|
1631
|
+
const { prepareWorkspaces, workspaceKey } = await import("./workspace-CyoXG6CY.js");
|
|
1632
|
+
const live = planned.filter((p) => !p.attempt.plan.skip);
|
|
1633
|
+
const outcomes = await prepareWorkspaces(live.flatMap(({ attempt: a }) => a.workspace ? [{
|
|
1634
|
+
workspace: a.workspace,
|
|
1635
|
+
environment: a.cell.environment
|
|
1636
|
+
}] : []), {
|
|
1637
|
+
root: config.root,
|
|
1638
|
+
runId
|
|
1639
|
+
});
|
|
1640
|
+
for (const o of outcomes) {
|
|
1641
|
+
const at = Date.now();
|
|
1642
|
+
if (o.ok) {
|
|
1643
|
+
const text = `workspace ${workspaceLabel(o.workspace)} at ${o.prepared.commit.slice(0, 12)}`;
|
|
1644
|
+
renderer.onEvent({
|
|
1645
|
+
type: "notice",
|
|
1646
|
+
at,
|
|
1647
|
+
level: "info",
|
|
1648
|
+
text
|
|
1649
|
+
});
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const key = workspaceKey(o.workspace);
|
|
1653
|
+
const user = live.find((p) => p.attempt.workspace && workspaceKey(p.attempt.workspace) === key);
|
|
1654
|
+
if (!user) continue;
|
|
1655
|
+
const target = {
|
|
1656
|
+
kind: "workspace",
|
|
1657
|
+
key: o.label,
|
|
1658
|
+
label: `workspace ${o.label}`,
|
|
1659
|
+
sandbox: user.attempt.sandbox,
|
|
1660
|
+
harness: user.attempt.harness,
|
|
1661
|
+
setBy: o.setBy,
|
|
1662
|
+
runWide: false
|
|
1663
|
+
};
|
|
1664
|
+
renderer.onEvent({
|
|
1665
|
+
type: "prepare",
|
|
1666
|
+
at,
|
|
1667
|
+
state: "failed",
|
|
1668
|
+
outcome: {
|
|
1669
|
+
target,
|
|
1670
|
+
ok: false,
|
|
1671
|
+
failure: o.failure,
|
|
1672
|
+
ms: 0
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
try {
|
|
1677
|
+
sandboxVersions = await preflight(planned.filter((p) => !p.attempt.plan.skip).map((p) => ({
|
|
1678
|
+
harness: p.attempt.harness,
|
|
1679
|
+
sandbox: p.attempt.sandbox,
|
|
1680
|
+
setBy: p.attempt.setBy
|
|
1681
|
+
})), {
|
|
1682
|
+
root: config.root,
|
|
1683
|
+
runId,
|
|
1684
|
+
onEvent: (e) => renderer.onEvent({
|
|
1685
|
+
type: "prepare",
|
|
1686
|
+
at: Date.now(),
|
|
1687
|
+
...e
|
|
1688
|
+
})
|
|
1689
|
+
});
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
if (error.name !== "PreflightInfraError") throw error;
|
|
1692
|
+
preflightStop = error;
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
return true;
|
|
1696
|
+
};
|
|
1697
|
+
const startBudget = () => {
|
|
1698
|
+
const { duration } = setup.budget;
|
|
1699
|
+
if (duration === void 0 || budgetTimer) return;
|
|
1700
|
+
budgetTimer = setTimeout(() => {
|
|
1701
|
+
reached = true;
|
|
1702
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
1703
|
+
writeFileSync(budgetMarker(stagingDir), `The run reached budget.duration ${duration} before this attempt started`);
|
|
1704
|
+
renderer.onEvent({
|
|
1705
|
+
type: "notice",
|
|
1706
|
+
at: Date.now(),
|
|
1707
|
+
level: "warn",
|
|
1708
|
+
text: `Budget: ${duration} reached; running attempts finish, queued ones are skipped`
|
|
1709
|
+
});
|
|
1710
|
+
}, parseDuration(duration));
|
|
1711
|
+
budgetTimer.unref();
|
|
1712
|
+
};
|
|
1713
|
+
let engineRun;
|
|
1714
|
+
try {
|
|
1715
|
+
engineRun = await runEngine({
|
|
1716
|
+
run: JSON.parse(JSON.stringify({
|
|
1717
|
+
runId,
|
|
1718
|
+
stagingDir,
|
|
1719
|
+
config,
|
|
1720
|
+
runs,
|
|
1721
|
+
tags,
|
|
1722
|
+
concurrency,
|
|
1723
|
+
profile,
|
|
1724
|
+
verbosity,
|
|
1725
|
+
configFile
|
|
1726
|
+
})),
|
|
1727
|
+
filters,
|
|
1728
|
+
onPlan,
|
|
1729
|
+
onRunStart: (files) => {
|
|
1730
|
+
live = writeLiveProgress({
|
|
1731
|
+
dir: stagingDir,
|
|
1732
|
+
runId,
|
|
1733
|
+
startedAt: ctx.startedAt,
|
|
1734
|
+
planned: toRun
|
|
1735
|
+
});
|
|
1736
|
+
renderer.onEvent({
|
|
1737
|
+
type: "run-start",
|
|
1738
|
+
at: Date.now(),
|
|
1739
|
+
version,
|
|
1740
|
+
files,
|
|
1741
|
+
harness: (plan?.harnesses ?? config.harness.map(harnessId)).join(", "),
|
|
1742
|
+
sandbox: sandboxId(config.sandbox),
|
|
1743
|
+
concurrency
|
|
1744
|
+
});
|
|
1745
|
+
},
|
|
1746
|
+
onEvent: (e) => {
|
|
1747
|
+
if (e.type === "eval-start") {
|
|
1748
|
+
startBudget();
|
|
1749
|
+
live?.start(e.key, e.at);
|
|
1750
|
+
}
|
|
1751
|
+
if (e.type === "eval-end") live?.end(e.result);
|
|
1752
|
+
if (interrupted && e.type === "eval-end" && e.result.state === "skipped") return;
|
|
1753
|
+
renderer.onEvent(e);
|
|
1754
|
+
},
|
|
1755
|
+
onCancellable: (cancel) => {
|
|
1756
|
+
const onSignal = (signal) => {
|
|
1757
|
+
stopCode = signal === "SIGTERM" ? EXIT.terminated : EXIT.interrupted;
|
|
1758
|
+
if (interrupted) {
|
|
1759
|
+
renderer.close();
|
|
1760
|
+
process.exit(stopCode);
|
|
1761
|
+
}
|
|
1762
|
+
interrupted = true;
|
|
1763
|
+
live?.phase("cancelling");
|
|
1764
|
+
renderer.onEvent({
|
|
1765
|
+
type: "notice",
|
|
1766
|
+
at: Date.now(),
|
|
1767
|
+
level: "warn",
|
|
1768
|
+
text: "Cancelling: stopping agents and destroying sandboxes (Ctrl-C again to force)"
|
|
1769
|
+
});
|
|
1770
|
+
cancel();
|
|
1771
|
+
};
|
|
1772
|
+
process.on("SIGINT", onSignal);
|
|
1773
|
+
process.on("SIGTERM", onSignal);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
live?.phase("finishing");
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
live?.remove();
|
|
1779
|
+
throw error;
|
|
1780
|
+
} finally {
|
|
1781
|
+
clearTimeout(budgetTimer);
|
|
1782
|
+
const { removePrepared } = await import("./preflight-BtubXKQ8.js");
|
|
1783
|
+
removePrepared(config.root, runId);
|
|
1784
|
+
const { removePreparedWorkspaces } = await import("./workspace-CyoXG6CY.js");
|
|
1785
|
+
removePreparedWorkspaces(config.root, runId);
|
|
1786
|
+
}
|
|
1787
|
+
if (preflightStop) throw preflightStop;
|
|
1788
|
+
if (engineRun.noFiles || !engineRun.stopped && engineRun.evals.length === 0 && !interrupted) live?.remove();
|
|
1789
|
+
if (engineRun.noFiles && !filters.length && !tags.length) throw new UsageError(`No eval files match ${config.include.join(", ")} (config.include) in ${config.root}. Name eval files to match it, or set include in ${config.file}.`);
|
|
1790
|
+
if (engineRun.noFiles || !engineRun.stopped && engineRun.evals.length === 0 && !interrupted) throw noMatch(config, filters, tags);
|
|
1791
|
+
return {
|
|
1792
|
+
engineRun,
|
|
1793
|
+
plan,
|
|
1794
|
+
sandboxVersions,
|
|
1795
|
+
budgetReached: () => reached,
|
|
1796
|
+
live
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
process.stdout.on("error", (error) => {
|
|
1800
|
+
if (error.code !== "EPIPE") throw error;
|
|
1801
|
+
});
|
|
1802
|
+
/**
|
|
1803
|
+
* Logs the command for `reeval report` and, when it looks like reeval broke (a crash, an unexpected
|
|
1804
|
+
* error or an infra error), prints one line suggesting `reeval report`. Not for agents or JSON.
|
|
1805
|
+
*/
|
|
1806
|
+
function finish(code, error, kind) {
|
|
1807
|
+
const flags = invocation.flags;
|
|
1808
|
+
let root = process.cwd();
|
|
1809
|
+
try {
|
|
1810
|
+
const configPath = flags ? configPathFor(flags.config) : findConfig();
|
|
1811
|
+
if (configPath) root = dirname(configPath);
|
|
1812
|
+
} catch {}
|
|
1813
|
+
const command = commandOf(argv);
|
|
1814
|
+
logInvocation(root, {
|
|
1815
|
+
at: new Date(startedAt).toISOString(),
|
|
1816
|
+
es: version,
|
|
1817
|
+
argv: redactArgv(argv),
|
|
1818
|
+
command,
|
|
1819
|
+
exitCode: code,
|
|
1820
|
+
durationMs: Date.now() - startedAt,
|
|
1821
|
+
runId: invocation.runId,
|
|
1822
|
+
error: error && kind ? {
|
|
1823
|
+
kind,
|
|
1824
|
+
name: error instanceof Error ? error.name : "Error",
|
|
1825
|
+
message: redactSecrets(error instanceof Error ? error.message : String(error)),
|
|
1826
|
+
stack: error instanceof Error && error.stack ? redactSecrets(error.stack) : null
|
|
1827
|
+
} : null,
|
|
1828
|
+
unhandledErrors: invocation.unhandledErrors.map((e) => redactSecrets(e))
|
|
1829
|
+
});
|
|
1830
|
+
const broke = kind === "crash" || code === EXIT.infra || invocation.unhandledErrors.length > 0;
|
|
1831
|
+
const quiet = !!process.env.REEVAL_NO_REPORT_HINT || command === "report" || !!flags?.json || flags?.ui === "agent" || process.env.REEVAL_UI === "agent" || !flags?.ui && !process.env.REEVAL_UI && !!detectAgent().name;
|
|
1832
|
+
if (broke && !quiet) console.error(dim("hint: if this looks like a bug in reeval, reeval report collects a redacted report to file (REEVAL_NO_REPORT_HINT=1 hides this)"));
|
|
1833
|
+
}
|
|
1834
|
+
const crash = (error) => {
|
|
1835
|
+
console.error(red(`reeval: ${redactSecrets(error instanceof Error ? error.message : String(error))}`));
|
|
1836
|
+
if (process.env.REEVAL_DEBUG) console.error(redactSecrets(inspect(error)));
|
|
1837
|
+
finish(EXIT.infra, error, "crash");
|
|
1838
|
+
process.exit(EXIT.infra);
|
|
1839
|
+
};
|
|
1840
|
+
process.on("unhandledRejection", crash);
|
|
1841
|
+
process.on("uncaughtException", crash);
|
|
1842
|
+
let settled = false;
|
|
1843
|
+
process.on("exit", (code) => {
|
|
1844
|
+
if (settled || code !== 0) return;
|
|
1845
|
+
process.stderr.write(red("reeval: stopped before it finished: nothing was left for it to wait on. That's a bug in reeval, or in code the config or an eval ran; reeval report collects a redacted report.\n"));
|
|
1846
|
+
process.exitCode = EXIT.infra;
|
|
1847
|
+
});
|
|
1848
|
+
run(process.argv.slice(2)).finally(() => {
|
|
1849
|
+
settled = true;
|
|
1850
|
+
}).then((code) => {
|
|
1851
|
+
finish(code);
|
|
1852
|
+
process.exitCode = code;
|
|
1853
|
+
}, (error) => {
|
|
1854
|
+
const name = error.name;
|
|
1855
|
+
const expected = error instanceof UsageError || EXPECTED_ERRORS.includes(name) || name === "PreflightInfraError";
|
|
1856
|
+
console.error(red(`reeval: ${redactSecrets(error.message)}`));
|
|
1857
|
+
if (!expected && process.env.REEVAL_DEBUG) console.error(redactSecrets(inspect(error)));
|
|
1858
|
+
const code = name === "PreflightInfraError" ? EXIT.infra : EXIT.usage;
|
|
1859
|
+
finish(code, error, error instanceof UsageError ? "usage" : expected ? "expected" : "crash");
|
|
1860
|
+
process.exitCode = code;
|
|
1861
|
+
});
|
|
1862
|
+
//#endregion
|
|
1863
|
+
export {};
|
|
1864
|
+
|
|
1865
|
+
//# sourceMappingURL=cli--CQ3Iy9q.js.map
|