@garuda-agent/garuda 0.18.0 → 0.18.2
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/CHANGELOG.md +156 -0
- package/README.md +3 -2
- package/dist/{acpCommand-IUZPLPLI.js → acpCommand-PUJHFWYU.js} +10 -8
- package/dist/advise-C777SZ5M.js +433 -0
- package/dist/brief-RWKESOPT.js +91 -0
- package/dist/buildCheck-HB46NWLX.js +19 -0
- package/dist/buildInfo-Z4P7QAQL.js +8 -0
- package/dist/chunk-2OYTNNUJ.js +216 -0
- package/dist/chunk-4BELTWP4.js +156 -0
- package/dist/chunk-4QRTTRVK.js +88 -0
- package/dist/chunk-6JQTPW3P.js +88 -0
- package/dist/chunk-A5DLAXB4.js +1780 -0
- package/dist/{chunk-FARS4R67.js → chunk-AX3A5KPH.js} +18 -1
- package/dist/chunk-CFDRP6XW.js +9 -0
- package/dist/chunk-E6YRCDSF.js +9 -0
- package/dist/{chunk-OQ26W5L4.js → chunk-HP6EGFHV.js} +132 -75
- package/dist/chunk-L55VUSX3.js +54 -0
- package/dist/{chunk-B4SJY7KT.js → chunk-LMKGSYJC.js} +77 -8
- package/dist/{chunk-Q4P3W657.js → chunk-LWNMEGDF.js} +10 -1124
- package/dist/{chunk-23FXBNBB.js → chunk-MNE7HHX2.js} +2 -1
- package/dist/{chunk-UKIE4AAM.js → chunk-NLS7GMF7.js} +11 -105
- package/dist/{chunk-6XHON3Q6.js → chunk-QXTHEFKP.js} +3 -3
- package/dist/{chunk-4BVCYCQT.js → chunk-R7RCW4BQ.js} +9 -7
- package/dist/{chunk-NHWHG3SW.js → chunk-STVFWSIL.js} +350 -57
- package/dist/{chunk-5B5HDTUR.js → chunk-TWQ7V2L6.js} +2 -2
- package/dist/chunk-ULAJPMY7.js +107 -0
- package/dist/chunk-VISKX36R.js +306 -0
- package/dist/{chunk-55CFEEIN.js → chunk-WK5NE6RY.js} +2 -2
- package/dist/chunk-Y62J7PTD.js +90 -0
- package/dist/chunk-YAQ6QLKB.js +40 -0
- package/dist/{chunk-FOPBOB7W.js → chunk-YEH2OONM.js} +6 -4
- package/dist/{chunk-7TH3NHCQ.js → chunk-ZKT4P2PC.js} +2 -2
- package/dist/cli/index.js +242 -52
- package/dist/{create-W6BV75QX.js → create-OACSGJ37.js} +5 -5
- package/dist/{evalCommand-SKJQ6CW2.js → evalCommand-MRO6ENCA.js} +77 -39
- package/dist/{evalTune-CZRCPSZ3.js → evalTune-VL5RHYOG.js} +24 -18
- package/dist/{export-SJ3DUP6W.js → export-ZPF3AMO6.js} +27 -4
- package/dist/exportHtml-ISISG5HG.js +21 -0
- package/dist/{inkChat-WYYAFZXD.js → inkChat-UDFX6GH6.js} +256 -41
- package/dist/{install-TKWN5AYH.js → install-4ST5BNRG.js} +3 -3
- package/dist/{jobCommand-2ODQLHB3.js → jobCommand-ILOTYOX3.js} +8 -7
- package/dist/{launchd-3A3ONEW5.js → launchd-QG6R5RAK.js} +4 -4
- package/dist/{lspCommand-ZZ6R4PA2.js → lspCommand-X4FTTONR.js} +3 -3
- package/dist/{manager-TCYTA5XU.js → manager-YH7722C2.js} +3 -3
- package/dist/{nightCommand-WJU6VZA6.js → nightCommand-XPWT6CQZ.js} +8 -7
- package/dist/receiptView-YFDHV253.js +19 -0
- package/dist/{remove-42WQNVPX.js → remove-STX5UO7X.js} +3 -3
- package/dist/{run-PTMRQCIU.js → run-W4TK52WT.js} +5 -5
- package/dist/runner-EBF6EOYM.js +97 -0
- package/dist/{server-VEYIJHEU.js → server-ASM73QVY.js} +3 -3
- package/dist/tour-CEVMAC5S.js +18 -0
- package/dist/tune-KSNKBPNJ.js +51 -0
- package/dist/uiState-YFJQ43D7.js +16 -0
- package/package.json +1 -1
- package/dist/chunk-4JSUUDFQ.js +0 -118
- package/dist/chunk-C57GHLKP.js +0 -9
- package/dist/chunk-DOCDCQVM.js +0 -484
- package/dist/tune-YVI2KWQF.js +0 -31
|
@@ -0,0 +1,1780 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AutoApprover
|
|
4
|
+
} from "./chunk-NXJ4XLB5.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_MAX_STEPS,
|
|
7
|
+
Runtime
|
|
8
|
+
} from "./chunk-STVFWSIL.js";
|
|
9
|
+
import {
|
|
10
|
+
FileSessionStore,
|
|
11
|
+
isCostIncomplete
|
|
12
|
+
} from "./chunk-NLS7GMF7.js";
|
|
13
|
+
import {
|
|
14
|
+
totalTokens
|
|
15
|
+
} from "./chunk-7KBOIPUT.js";
|
|
16
|
+
import {
|
|
17
|
+
LINK_CANDIDATES,
|
|
18
|
+
cleanArgs
|
|
19
|
+
} from "./chunk-WK5NE6RY.js";
|
|
20
|
+
import {
|
|
21
|
+
MANIFEST,
|
|
22
|
+
TEST_PATH,
|
|
23
|
+
tail
|
|
24
|
+
} from "./chunk-AOGFYUKG.js";
|
|
25
|
+
import {
|
|
26
|
+
git,
|
|
27
|
+
shellWord
|
|
28
|
+
} from "./chunk-QXTHEFKP.js";
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_ENV_ALLOWLIST,
|
|
31
|
+
HostExecutor,
|
|
32
|
+
denyAround,
|
|
33
|
+
parseSettings
|
|
34
|
+
} from "./chunk-LMKGSYJC.js";
|
|
35
|
+
import {
|
|
36
|
+
pathMatches
|
|
37
|
+
} from "./chunk-3NJNVZTK.js";
|
|
38
|
+
|
|
39
|
+
// src/evals/runner.ts
|
|
40
|
+
import { constants as fsConstants, mkdirSync as mkdirSync3, realpathSync as realpathSync2 } from "fs";
|
|
41
|
+
import { copyFile, mkdtemp as mkdtemp3, readFile as readFile4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
42
|
+
import { homedir, tmpdir as tmpdir4 } from "os";
|
|
43
|
+
import { join as join7 } from "path";
|
|
44
|
+
|
|
45
|
+
// src/evals/agents/claudeCode.ts
|
|
46
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
47
|
+
import { tmpdir } from "os";
|
|
48
|
+
import { join } from "path";
|
|
49
|
+
var MAX_OUTPUT_BYTES = 5e6;
|
|
50
|
+
function claudeCommand(agent, prompt, maxTurns) {
|
|
51
|
+
return [
|
|
52
|
+
quote(agent.command),
|
|
53
|
+
"-p",
|
|
54
|
+
quote(prompt),
|
|
55
|
+
"--output-format",
|
|
56
|
+
"json",
|
|
57
|
+
"--model",
|
|
58
|
+
quote(agent.model),
|
|
59
|
+
"--max-turns",
|
|
60
|
+
String(maxTurns),
|
|
61
|
+
"--permission-mode",
|
|
62
|
+
"bypassPermissions"
|
|
63
|
+
].join(" ");
|
|
64
|
+
}
|
|
65
|
+
function parseClaudeResult(stdout) {
|
|
66
|
+
const line = stdout.trim().split("\n").reverse().find((l) => l.trim().startsWith("{"));
|
|
67
|
+
let data;
|
|
68
|
+
try {
|
|
69
|
+
data = JSON.parse(line ?? "");
|
|
70
|
+
} catch {
|
|
71
|
+
return {
|
|
72
|
+
stopReason: "error",
|
|
73
|
+
steps: 0,
|
|
74
|
+
tokens: 0,
|
|
75
|
+
cacheReadTokens: 0,
|
|
76
|
+
costUsd: void 0,
|
|
77
|
+
reason: `Claude Code gave no JSON result: ${stdout.trim().slice(0, 300) || "no output"}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const usage = data.usage ?? {};
|
|
81
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
82
|
+
const cacheRead = n(usage.cache_read_input_tokens);
|
|
83
|
+
const tokens = n(usage.input_tokens) + n(usage.output_tokens) + cacheRead + n(usage.cache_creation_input_tokens);
|
|
84
|
+
const costUsd = typeof data.total_cost_usd === "number" ? data.total_cost_usd : void 0;
|
|
85
|
+
const steps = n(data.num_turns);
|
|
86
|
+
const subtype = typeof data.subtype === "string" ? data.subtype : "";
|
|
87
|
+
const base = { steps, tokens, cacheReadTokens: cacheRead, costUsd };
|
|
88
|
+
if (subtype === "error_max_turns") return { ...base, stopReason: "max_steps" };
|
|
89
|
+
if (subtype === "success" && data.is_error !== true) return { ...base, stopReason: "done" };
|
|
90
|
+
const text = typeof data.result === "string" ? data.result : subtype || "unknown error";
|
|
91
|
+
return { ...base, stopReason: "error", reason: `Claude Code failed: ${text.slice(0, 300)}` };
|
|
92
|
+
}
|
|
93
|
+
async function runClaudeCode(executor, policy, agent, prompt, maxTurns, signal) {
|
|
94
|
+
const config = await mkdtemp(join(tmpdir(), "garuda-claude-config-"));
|
|
95
|
+
try {
|
|
96
|
+
await writeFile(
|
|
97
|
+
join(config, ".claude.json"),
|
|
98
|
+
`${JSON.stringify({ hasCompletedOnboarding: true })}
|
|
99
|
+
`
|
|
100
|
+
);
|
|
101
|
+
const result = await executor.run(
|
|
102
|
+
claudeCommand(agent, prompt, maxTurns),
|
|
103
|
+
{ ...policy, maxOutputBytes: MAX_OUTPUT_BYTES },
|
|
104
|
+
{
|
|
105
|
+
signal,
|
|
106
|
+
env: {
|
|
107
|
+
...agent.env,
|
|
108
|
+
CLAUDE_CONFIG_DIR: config,
|
|
109
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
110
|
+
DISABLE_AUTOUPDATER: "1"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
const outcome = result.timedOut || result.aborted ? {
|
|
115
|
+
stopReason: "timeout",
|
|
116
|
+
steps: 0,
|
|
117
|
+
tokens: 0,
|
|
118
|
+
cacheReadTokens: 0,
|
|
119
|
+
costUsd: void 0,
|
|
120
|
+
reason: "Claude Code took longer than the task's time limit."
|
|
121
|
+
} : parseClaudeResult(result.stdout.text);
|
|
122
|
+
if (outcome.stopReason === "error" && result.stderr.text.trim() !== "" && outcome.reason !== void 0) {
|
|
123
|
+
outcome.reason += `
|
|
124
|
+
${result.stderr.text.trim().split("\n").slice(-3).join("\n")}`;
|
|
125
|
+
}
|
|
126
|
+
return { result, outcome };
|
|
127
|
+
} finally {
|
|
128
|
+
await rm(config, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function quote(text) {
|
|
132
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/evals/changes.ts
|
|
136
|
+
import { createHash } from "crypto";
|
|
137
|
+
import { lstat, readdir, readFile, readlink } from "fs/promises";
|
|
138
|
+
import { join as join2 } from "path";
|
|
139
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
140
|
+
".git",
|
|
141
|
+
".garuda",
|
|
142
|
+
// Claude Code's own project folder (the T11b baseline), as .garuda is Garuda's.
|
|
143
|
+
".claude",
|
|
144
|
+
"node_modules",
|
|
145
|
+
"target",
|
|
146
|
+
"build",
|
|
147
|
+
"dist",
|
|
148
|
+
"__pycache__",
|
|
149
|
+
".pytest_cache"
|
|
150
|
+
]);
|
|
151
|
+
var IGNORED_FILE = /\.(class|pyc)$/;
|
|
152
|
+
var MAX_TRACKED_FILES = 5e4;
|
|
153
|
+
async function treeState(root) {
|
|
154
|
+
const out = /* @__PURE__ */ new Map();
|
|
155
|
+
const walk = async (dir, prefix) => {
|
|
156
|
+
let names;
|
|
157
|
+
try {
|
|
158
|
+
names = await readdir(dir);
|
|
159
|
+
} catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const name of names.sort()) {
|
|
163
|
+
if (out.size >= MAX_TRACKED_FILES) return;
|
|
164
|
+
const path = prefix === "" ? name : `${prefix}/${name}`;
|
|
165
|
+
const full = join2(dir, name);
|
|
166
|
+
const info = await lstat(full).catch(() => void 0);
|
|
167
|
+
if (info === void 0) continue;
|
|
168
|
+
if (info.isDirectory()) {
|
|
169
|
+
if (!IGNORED_DIRS.has(name)) await walk(full, path);
|
|
170
|
+
} else if (info.isSymbolicLink()) {
|
|
171
|
+
out.set(path, `link:${await readlink(full).catch(() => "")}`);
|
|
172
|
+
} else if (info.isFile() && !IGNORED_FILE.test(name)) {
|
|
173
|
+
const hash = createHash("sha256");
|
|
174
|
+
hash.update(await readFile(full).catch(() => Buffer.alloc(0)));
|
|
175
|
+
out.set(path, hash.digest("hex"));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
await walk(root, "");
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function changedPaths(before, after) {
|
|
183
|
+
const out = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const [path, hash] of after) if (before.get(path) !== hash) out.add(path);
|
|
185
|
+
for (const path of before.keys()) if (!after.has(path)) out.add(path);
|
|
186
|
+
return [...out].sort();
|
|
187
|
+
}
|
|
188
|
+
function unexpectedPaths(changed, expected, allowChanges = []) {
|
|
189
|
+
const known = new Set(expected);
|
|
190
|
+
return changed.filter(
|
|
191
|
+
(path) => !known.has(path) && !allowChanges.some((glob) => pathMatches(glob, path, false))
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/evals/compare.ts
|
|
196
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
197
|
+
import { join as join3 } from "path";
|
|
198
|
+
|
|
199
|
+
// src/evals/stats.ts
|
|
200
|
+
var BOOTSTRAP_DRAWS = 1e4;
|
|
201
|
+
var DEFAULT_SEED = 1;
|
|
202
|
+
function seeded(seed) {
|
|
203
|
+
let a = seed >>> 0;
|
|
204
|
+
return () => {
|
|
205
|
+
a = a + 1831565813 >>> 0;
|
|
206
|
+
let t = a;
|
|
207
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
208
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
209
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function shuffled(items, seed) {
|
|
213
|
+
const out = [...items];
|
|
214
|
+
const random = seeded(seed);
|
|
215
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
216
|
+
const j = Math.floor(random() * (i + 1));
|
|
217
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
function byTask(results) {
|
|
222
|
+
const map = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const r of results) {
|
|
224
|
+
let t = map.get(r.id);
|
|
225
|
+
if (t === void 0) {
|
|
226
|
+
t = { id: r.id, runs: 0, passes: 0, errors: 0, results: [] };
|
|
227
|
+
map.set(r.id, t);
|
|
228
|
+
}
|
|
229
|
+
t.runs++;
|
|
230
|
+
if (r.passed) t.passes++;
|
|
231
|
+
if (r.stopReason === "error") t.errors++;
|
|
232
|
+
t.results.push(r);
|
|
233
|
+
}
|
|
234
|
+
return [...map.values()];
|
|
235
|
+
}
|
|
236
|
+
function passInterval(results, options = {}) {
|
|
237
|
+
const tasks = byTask(results);
|
|
238
|
+
const runs = tasks.reduce((s, t) => s + t.runs, 0);
|
|
239
|
+
if (tasks.length === 0 || runs === 0) return void 0;
|
|
240
|
+
const seed = options.seed ?? DEFAULT_SEED;
|
|
241
|
+
const draws = options.draws ?? BOOTSTRAP_DRAWS;
|
|
242
|
+
const level = options.level ?? 0.95;
|
|
243
|
+
const random = seeded(seed);
|
|
244
|
+
const rates = new Float64Array(draws);
|
|
245
|
+
for (let d = 0; d < draws; d++) {
|
|
246
|
+
let passes = 0;
|
|
247
|
+
let n = 0;
|
|
248
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
249
|
+
const t = tasks[Math.floor(random() * tasks.length)];
|
|
250
|
+
passes += t.passes;
|
|
251
|
+
n += t.runs;
|
|
252
|
+
}
|
|
253
|
+
rates[d] = passes / n;
|
|
254
|
+
}
|
|
255
|
+
rates.sort();
|
|
256
|
+
const tail2 = (1 - level) / 2;
|
|
257
|
+
return {
|
|
258
|
+
rate: tasks.reduce((s, t) => s + t.passes, 0) / runs,
|
|
259
|
+
low: quantile(rates, tail2),
|
|
260
|
+
high: quantile(rates, 1 - tail2),
|
|
261
|
+
level,
|
|
262
|
+
draws,
|
|
263
|
+
seed,
|
|
264
|
+
tasks: tasks.length
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function consistency(results) {
|
|
268
|
+
const repeated = byTask(results).filter((t) => t.runs >= 2);
|
|
269
|
+
return {
|
|
270
|
+
repeated: repeated.length,
|
|
271
|
+
allPassed: repeated.filter((t) => t.passes === t.runs).map((t) => t.id),
|
|
272
|
+
nonePassed: repeated.filter((t) => t.passes === 0 && t.errors < t.runs).map((t) => t.id),
|
|
273
|
+
onlyErrors: repeated.filter((t) => t.errors === t.runs).map((t) => t.id)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function spread(results) {
|
|
277
|
+
const scored = results.filter((r) => r.stopReason !== "error");
|
|
278
|
+
if (scored.length === 0) return void 0;
|
|
279
|
+
const of = (f) => {
|
|
280
|
+
const sorted = Float64Array.from(scored.map(f)).sort();
|
|
281
|
+
return { median: quantile(sorted, 0.5), p95: quantile(sorted, 0.95) };
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
runs: scored.length,
|
|
285
|
+
steps: of((r) => r.steps),
|
|
286
|
+
tokens: of((r) => r.tokens),
|
|
287
|
+
durationMs: of((r) => r.durationMs)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function stepsSd(runs) {
|
|
291
|
+
const steps = runs.filter((r) => r.stopReason !== "error").map((r) => r.steps);
|
|
292
|
+
if (steps.length < 2) return void 0;
|
|
293
|
+
const mean = steps.reduce((s, x) => s + x, 0) / steps.length;
|
|
294
|
+
return Math.sqrt(steps.reduce((s, x) => s + (x - mean) ** 2, 0) / (steps.length - 1));
|
|
295
|
+
}
|
|
296
|
+
function costPerPass(results) {
|
|
297
|
+
if (results.some((r) => r.costUsd === void 0)) return { kind: "unknown" };
|
|
298
|
+
const totalUsd = results.reduce((s, r) => s + (r.costUsd ?? 0), 0);
|
|
299
|
+
const passes = results.filter((r) => r.passed).length;
|
|
300
|
+
if (passes === 0) return { kind: "none", totalUsd };
|
|
301
|
+
return {
|
|
302
|
+
kind: "value",
|
|
303
|
+
usd: totalUsd / passes,
|
|
304
|
+
minimum: results.some((r) => r.costIncomplete === true),
|
|
305
|
+
totalUsd,
|
|
306
|
+
passes
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function unexpectedChanges(results) {
|
|
310
|
+
const measured = results.filter((r) => r.unexpected !== void 0);
|
|
311
|
+
return {
|
|
312
|
+
runs: measured.filter((r) => (r.unexpected?.length ?? 0) > 0).length,
|
|
313
|
+
files: measured.reduce((s, r) => s + (r.unexpected?.length ?? 0), 0),
|
|
314
|
+
measured: measured.length
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function quantile(sorted, q) {
|
|
318
|
+
if (sorted.length === 0) return Number.NaN;
|
|
319
|
+
const pos = (sorted.length - 1) * q;
|
|
320
|
+
const lo = Math.floor(pos);
|
|
321
|
+
const hi = Math.ceil(pos);
|
|
322
|
+
const a = sorted[lo];
|
|
323
|
+
const b = sorted[hi];
|
|
324
|
+
return a + (b - a) * (pos - lo);
|
|
325
|
+
}
|
|
326
|
+
function mcpUse(results) {
|
|
327
|
+
const measured = results.filter((r) => r.mcpCalls !== void 0);
|
|
328
|
+
if (measured.length === 0) return void 0;
|
|
329
|
+
return {
|
|
330
|
+
calls: measured.reduce((sum, r) => sum + (r.mcpCalls ?? 0), 0),
|
|
331
|
+
runs: measured.filter((r) => (r.mcpCalls ?? 0) > 0).length,
|
|
332
|
+
measured: measured.length
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function mcpUseText(use) {
|
|
336
|
+
return `${use.calls} in ${use.runs} of ${use.measured} runs${use.runs === 0 ? " (the agent never used the server)" : ""}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/evals/compare.ts
|
|
340
|
+
async function loadReport(path) {
|
|
341
|
+
const file = (await stat(path)).isDirectory() ? join3(path, "report.json") : path;
|
|
342
|
+
const data = JSON.parse(await readFile2(file, "utf8"));
|
|
343
|
+
return parseReport(data, file);
|
|
344
|
+
}
|
|
345
|
+
function parseReport(data, path) {
|
|
346
|
+
if (!Array.isArray(data.results)) throw new Error(`${path} is not an eval report (no results).`);
|
|
347
|
+
const schema = typeof data.schema === "number" ? data.schema : 1;
|
|
348
|
+
const tasks = schema >= 2 && data.tasks !== null && typeof data.tasks === "object" ? data.tasks : void 0;
|
|
349
|
+
return {
|
|
350
|
+
path,
|
|
351
|
+
schema,
|
|
352
|
+
model: typeof data.spec === "string" ? data.spec : typeof data.model === "string" ? data.model : void 0,
|
|
353
|
+
seed: typeof data.seed === "number" ? data.seed : void 0,
|
|
354
|
+
tasks,
|
|
355
|
+
results: data.results,
|
|
356
|
+
mcp: mcpServerOf(data.mcp)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function mcpServerOf(value) {
|
|
360
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
361
|
+
const server = value.server;
|
|
362
|
+
return typeof server === "string" ? server : void 0;
|
|
363
|
+
}
|
|
364
|
+
function compareReports(a, b, options = {}) {
|
|
365
|
+
const problems = [];
|
|
366
|
+
const notes = [];
|
|
367
|
+
const idsA = [...new Set(a.results.map((r) => r.id))].sort();
|
|
368
|
+
const idsB = [...new Set(b.results.map((r) => r.id))].sort();
|
|
369
|
+
if (idsA.join("\n") !== idsB.join("\n")) {
|
|
370
|
+
const onlyA = idsA.filter((id) => !idsB.includes(id));
|
|
371
|
+
const onlyB = idsB.filter((id) => !idsA.includes(id));
|
|
372
|
+
problems.push(
|
|
373
|
+
`The reports ran different tasks${onlyA.length > 0 ? `; only in A: ${onlyA.join(", ")}` : ""}${onlyB.length > 0 ? `; only in B: ${onlyB.join(", ")}` : ""}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (a.tasks !== void 0 && b.tasks !== void 0) {
|
|
377
|
+
const changed = idsA.filter((id) => idsB.includes(id) && a.tasks?.[id] !== b.tasks?.[id]);
|
|
378
|
+
if (changed.length > 0) {
|
|
379
|
+
problems.push(
|
|
380
|
+
`These tasks changed between the runs (their hashes differ): ${changed.join(", ")}. Run both arms on the same tasks.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
const old = [a, b].filter((r) => r.tasks === void 0).map((r) => r.path);
|
|
385
|
+
notes.push(
|
|
386
|
+
`${old.join(" and ")} ${old.length === 1 ? "has" : "have"} no task hashes (a report before 0.17): Garuda cannot check that the tasks were the same.`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (problems.length > 0) return { problems };
|
|
390
|
+
if (idsA.length === 0) return { problems: ["The reports have no runs."] };
|
|
391
|
+
const tasksA = new Map(byTask(a.results).map((t) => [t.id, t]));
|
|
392
|
+
const tasksB = new Map(byTask(b.results).map((t) => [t.id, t]));
|
|
393
|
+
const rows = idsA.map((id) => {
|
|
394
|
+
const ta = tasksA.get(id);
|
|
395
|
+
const tb = tasksB.get(id);
|
|
396
|
+
const arm = (t) => {
|
|
397
|
+
const results = t?.results ?? [];
|
|
398
|
+
return {
|
|
399
|
+
passes: t?.passes ?? 0,
|
|
400
|
+
runs: t?.runs ?? 0,
|
|
401
|
+
costUsd: results.some((r) => r.costUsd === void 0) ? void 0 : results.reduce((s, r) => s + (r.costUsd ?? 0), 0),
|
|
402
|
+
minimum: results.some((r) => r.costIncomplete === true)
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
return { id, a: arm(ta), b: arm(tb) };
|
|
406
|
+
});
|
|
407
|
+
const rate = (side, list) => list.reduce((s, r) => s + r[side].passes, 0) / list.reduce((s, r) => s + r[side].runs, 0);
|
|
408
|
+
const rateA = rate("a", rows);
|
|
409
|
+
const rateB = rate("b", rows);
|
|
410
|
+
const seed = options.seed ?? DEFAULT_SEED;
|
|
411
|
+
const draws = options.draws ?? BOOTSTRAP_DRAWS;
|
|
412
|
+
const level = options.level ?? 0.95;
|
|
413
|
+
const random = seeded(seed);
|
|
414
|
+
const diffs = new Float64Array(draws);
|
|
415
|
+
for (let d = 0; d < draws; d++) {
|
|
416
|
+
let pa = 0;
|
|
417
|
+
let na = 0;
|
|
418
|
+
let pb = 0;
|
|
419
|
+
let nb = 0;
|
|
420
|
+
for (let i = 0; i < rows.length; i++) {
|
|
421
|
+
const r = rows[Math.floor(random() * rows.length)];
|
|
422
|
+
pa += r.a.passes;
|
|
423
|
+
na += r.a.runs;
|
|
424
|
+
pb += r.b.passes;
|
|
425
|
+
nb += r.b.runs;
|
|
426
|
+
}
|
|
427
|
+
diffs[d] = pb / nb - pa / na;
|
|
428
|
+
}
|
|
429
|
+
diffs.sort();
|
|
430
|
+
const tail2 = (1 - level) / 2;
|
|
431
|
+
const low = quantile(diffs, tail2);
|
|
432
|
+
const high = quantile(diffs, 1 - tail2);
|
|
433
|
+
const verdict = low > 0 ? "B passes more" : high < 0 ? "B passes less" : "no clear difference";
|
|
434
|
+
if (low === high) {
|
|
435
|
+
notes.push(
|
|
436
|
+
"Every draw gave the same pass-rate difference, so the interval shows no uncertainty: more tasks, or tasks that sometimes fail, are needed to measure it."
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (rows.length < 10) notes.push(roughNote(rows.length));
|
|
440
|
+
if (a.seed !== void 0 && b.seed !== void 0 && a.seed !== b.seed) {
|
|
441
|
+
notes.push(
|
|
442
|
+
`The arms ran with different seeds (${a.seed} and ${b.seed}): their run orders differ.`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (a.mcp !== b.mcp) {
|
|
446
|
+
notes.push(
|
|
447
|
+
`The arms differ in the MCP server: A ${a.mcp ?? "none"}, B ${b.mcp ?? "none"}. For an MCP A/B, keep every other option the same.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const cost = costRatio(rows, seed, draws, level);
|
|
451
|
+
if (typeof cost === "string") notes.push(cost);
|
|
452
|
+
else if (cost.skipped > draws * 0.05) {
|
|
453
|
+
notes.push(
|
|
454
|
+
`The cost interval skipped ${cost.skipped} of ${draws} draws (an arm had no pass in them).`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (rows.some((r) => r.a.minimum || r.b.minimum)) {
|
|
458
|
+
notes.push("A run's cost is a minimum (no reported usage): the cost ratio is approximate.");
|
|
459
|
+
}
|
|
460
|
+
const runsA = rows.map((r) => r.a.runs);
|
|
461
|
+
const runsB = rows.map((r) => r.b.runs);
|
|
462
|
+
if (Math.min(...runsA, ...runsB) < 3) {
|
|
463
|
+
notes.push(
|
|
464
|
+
"Some tasks have fewer than 3 runs in an arm: repeat each task 3 times or more (--repeat 3)."
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
comparison: {
|
|
469
|
+
rows,
|
|
470
|
+
rateA,
|
|
471
|
+
rateB,
|
|
472
|
+
difference: { value: rateB - rateA, low, high, level, draws, seed },
|
|
473
|
+
verdict,
|
|
474
|
+
...typeof cost === "string" ? {} : { costRatio: cost },
|
|
475
|
+
notes
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function costRatio(rows, seed, draws, level) {
|
|
480
|
+
if (rows.some((r) => r.a.costUsd === void 0 || r.b.costUsd === void 0)) {
|
|
481
|
+
return "A run's cost is unknown: no cost comparison.";
|
|
482
|
+
}
|
|
483
|
+
const per = (list, side) => {
|
|
484
|
+
const passes = list.reduce((s, r) => s + r[side].passes, 0);
|
|
485
|
+
const cost = list.reduce((s, r) => s + (r[side].costUsd ?? 0), 0);
|
|
486
|
+
return passes === 0 ? void 0 : cost / passes;
|
|
487
|
+
};
|
|
488
|
+
const a = per(rows, "a");
|
|
489
|
+
const b = per(rows, "b");
|
|
490
|
+
if (a === void 0 || b === void 0 || a === 0) {
|
|
491
|
+
return "An arm has no pass (or no cost): no cost ratio.";
|
|
492
|
+
}
|
|
493
|
+
const random = seeded(seed);
|
|
494
|
+
const ratios = [];
|
|
495
|
+
const drawn = new Array(rows.length);
|
|
496
|
+
for (let d = 0; d < draws; d++) {
|
|
497
|
+
for (let i = 0; i < rows.length; i++) {
|
|
498
|
+
drawn[i] = rows[Math.floor(random() * rows.length)];
|
|
499
|
+
}
|
|
500
|
+
const ca = per(drawn, "a");
|
|
501
|
+
const cb = per(drawn, "b");
|
|
502
|
+
if (ca !== void 0 && cb !== void 0 && ca > 0) ratios.push(cb / ca);
|
|
503
|
+
}
|
|
504
|
+
const sorted = Float64Array.from(ratios).sort();
|
|
505
|
+
const tail2 = (1 - level) / 2;
|
|
506
|
+
const low = quantile(sorted, tail2);
|
|
507
|
+
const high = quantile(sorted, 1 - tail2);
|
|
508
|
+
return {
|
|
509
|
+
value: b / a,
|
|
510
|
+
low,
|
|
511
|
+
high,
|
|
512
|
+
draws,
|
|
513
|
+
skipped: draws - ratios.length,
|
|
514
|
+
verdict: low > 1 ? "B costs more per accepted change" : high < 1 ? "B costs less per accepted change" : "no clear difference"
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function roughNote(tasks) {
|
|
518
|
+
return `Only ${tasks} task${tasks === 1 ? "" : "s"}: with fewer than 10 tasks the interval is a rough guide.`;
|
|
519
|
+
}
|
|
520
|
+
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
521
|
+
var signed = (x) => {
|
|
522
|
+
const points = Math.round(Math.abs(x) * 100);
|
|
523
|
+
return points === 0 ? "0 pts" : `${x > 0 ? "+" : "\u2212"}${points} pts`;
|
|
524
|
+
};
|
|
525
|
+
function formatComparison(a, b, c) {
|
|
526
|
+
const width = Math.max(4, ...c.rows.map((r) => r.id.length));
|
|
527
|
+
const lines2 = [
|
|
528
|
+
`A: ${a.path}${a.model === void 0 ? "" : ` (${a.model})`}`,
|
|
529
|
+
`B: ${b.path}${b.model === void 0 ? "" : ` (${b.model})`}`,
|
|
530
|
+
"",
|
|
531
|
+
`${"Task".padEnd(width)} A B`,
|
|
532
|
+
...c.rows.map(
|
|
533
|
+
(r) => `${r.id.padEnd(width)} ${`${r.a.passes}/${r.a.runs}`.padEnd(7)} ${r.b.passes}/${r.b.runs}`
|
|
534
|
+
),
|
|
535
|
+
"",
|
|
536
|
+
`Pass rate over all attempts: A ${pct(c.rateA)}, B ${pct(c.rateB)}; B \u2212 A ${signed(c.difference.value)} (95% paired interval ${signed(c.difference.low)} to ${signed(c.difference.high)}; ${c.difference.draws} draws, seed ${c.difference.seed})`,
|
|
537
|
+
`Verdict: ${c.verdict}${c.verdict === "no clear difference" ? " (the interval contains 0)" : ""}.`
|
|
538
|
+
];
|
|
539
|
+
for (const [name, report] of [
|
|
540
|
+
["A", a],
|
|
541
|
+
["B", b]
|
|
542
|
+
]) {
|
|
543
|
+
const cost = costPerPass(report.results);
|
|
544
|
+
const u = unexpectedChanges(report.results);
|
|
545
|
+
const s = spread(report.results);
|
|
546
|
+
const mcp = mcpUse(report.results);
|
|
547
|
+
lines2.push(
|
|
548
|
+
`${name}: cost per accepted change ${cost.kind === "unknown" ? "unknown" : cost.kind === "none" ? "no passes" : `${cost.minimum ? "\u2265 " : ""}$${cost.usd.toFixed(4)}`} \xB7 unexpected changes ${u.measured === 0 ? "not measured" : `${u.runs} of ${u.measured} runs`} \xB7 median time ${s === void 0 ? "\u2013" : `${Math.round(s.durationMs.median / 1e3)} s`}${mcp === void 0 ? "" : ` \xB7 MCP tool calls ${mcpUseText(mcp)}`}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (c.costRatio !== void 0) {
|
|
552
|
+
const x = (v) => `\xD7${v.toFixed(2)}`;
|
|
553
|
+
lines2.push(
|
|
554
|
+
`Cost per accepted change: B/A ${x(c.costRatio.value)} (95% paired interval ${x(c.costRatio.low)} to ${x(c.costRatio.high)}). Verdict: ${c.costRatio.verdict}${c.costRatio.verdict === "no clear difference" ? " (the interval contains \xD71.00)" : ""}.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
for (const note of c.notes) lines2.push(`Note: ${note}`);
|
|
558
|
+
return lines2.join("\n");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/evals/files.ts
|
|
562
|
+
import { mkdirSync } from "fs";
|
|
563
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
564
|
+
import { dirname, join as join4 } from "path";
|
|
565
|
+
async function writeFiles(root, files) {
|
|
566
|
+
for (const [path, content] of Object.entries(files)) {
|
|
567
|
+
const full = join4(root, path);
|
|
568
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
569
|
+
await writeFile2(full, content);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/evals/projects.ts
|
|
574
|
+
var MAVEN_POM = `<?xml version="1.0" encoding="UTF-8"?>
|
|
575
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
576
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
577
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
578
|
+
<modelVersion>4.0.0</modelVersion>
|
|
579
|
+
<groupId>dev.garuda.eval</groupId>
|
|
580
|
+
<artifactId>eval</artifactId>
|
|
581
|
+
<version>1.0.0</version>
|
|
582
|
+
|
|
583
|
+
<properties>
|
|
584
|
+
<maven.compiler.release>17</maven.compiler.release>
|
|
585
|
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
586
|
+
</properties>
|
|
587
|
+
|
|
588
|
+
<dependencies>
|
|
589
|
+
<dependency>
|
|
590
|
+
<groupId>org.junit.jupiter</groupId>
|
|
591
|
+
<artifactId>junit-jupiter</artifactId>
|
|
592
|
+
<version>5.11.4</version>
|
|
593
|
+
<scope>test</scope>
|
|
594
|
+
</dependency>
|
|
595
|
+
</dependencies>
|
|
596
|
+
|
|
597
|
+
<build>
|
|
598
|
+
<plugins>
|
|
599
|
+
<plugin>
|
|
600
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
601
|
+
<artifactId>maven-resources-plugin</artifactId>
|
|
602
|
+
<version>3.3.1</version>
|
|
603
|
+
</plugin>
|
|
604
|
+
<plugin>
|
|
605
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
606
|
+
<artifactId>maven-compiler-plugin</artifactId>
|
|
607
|
+
<version>3.13.0</version>
|
|
608
|
+
</plugin>
|
|
609
|
+
<plugin>
|
|
610
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
611
|
+
<artifactId>maven-surefire-plugin</artifactId>
|
|
612
|
+
<version>3.5.2</version>
|
|
613
|
+
</plugin>
|
|
614
|
+
</plugins>
|
|
615
|
+
</build>
|
|
616
|
+
</project>
|
|
617
|
+
`;
|
|
618
|
+
var MAVEN_CHECK = "test ! -e .mvn && mvn -B -q -o test";
|
|
619
|
+
var PYPROJECT = `[project]
|
|
620
|
+
name = "evalpkg"
|
|
621
|
+
version = "0.1.0"
|
|
622
|
+
requires-python = ">=3.9"
|
|
623
|
+
|
|
624
|
+
[tool.pytest.ini_options]
|
|
625
|
+
pythonpath = ["src"]
|
|
626
|
+
testpaths = ["tests"]
|
|
627
|
+
`;
|
|
628
|
+
var PYTEST_CHECK = "python3 -m pytest -q -c pyproject.toml --noconftest -p no:cacheprovider";
|
|
629
|
+
function isProtectedPath(path) {
|
|
630
|
+
return /^(test|tests|src\/test)\//.test(path) || path === "pom.xml" || path === "pyproject.toml";
|
|
631
|
+
}
|
|
632
|
+
var lines = (...l) => `${l.join("\n")}
|
|
633
|
+
`;
|
|
634
|
+
|
|
635
|
+
// src/evals/repoTasks.ts
|
|
636
|
+
import { mkdirSync as mkdirSync2, mkdtempSync, realpathSync } from "fs";
|
|
637
|
+
import { mkdir, readFile as readFile3, rm as rm3, symlink, writeFile as writeFile3 } from "fs/promises";
|
|
638
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
639
|
+
import { dirname as dirname2, join as join6, sep } from "path";
|
|
640
|
+
import { z } from "zod";
|
|
641
|
+
|
|
642
|
+
// src/evals/toolchains.ts
|
|
643
|
+
import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
644
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
645
|
+
import { join as join5 } from "path";
|
|
646
|
+
var TOOLCHAIN_ENV = ["JAVA_HOME", "GRADLE_USER_HOME", "VIRTUAL_ENV"];
|
|
647
|
+
var TOOLCHAINS = {
|
|
648
|
+
maven: {
|
|
649
|
+
id: "maven",
|
|
650
|
+
title: "Maven with JUnit 5 in ~/.m2",
|
|
651
|
+
files: {
|
|
652
|
+
"pom.xml": MAVEN_POM,
|
|
653
|
+
"src/main/java/probe/Probe.java": "package probe;\n\npublic final class Probe {\n public static int one() {\n return 1;\n }\n}\n",
|
|
654
|
+
"src/test/java/probe/ProbeTest.java": "package probe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.Test;\n\nclass ProbeTest {\n @Test\n void one() {\n assertEquals(1, Probe.one());\n }\n}\n"
|
|
655
|
+
},
|
|
656
|
+
probe: "mvn -B -q -o test",
|
|
657
|
+
prepare: "mvn -B -q test",
|
|
658
|
+
hint: "The Java tasks need a JDK (17 or later), Maven, and JUnit 5 in ~/.m2. Run `garuda eval --prepare java` once: it downloads the plugins and JUnit with Maven (network)."
|
|
659
|
+
},
|
|
660
|
+
pytest: {
|
|
661
|
+
id: "pytest",
|
|
662
|
+
title: "Python 3 with pytest",
|
|
663
|
+
files: {
|
|
664
|
+
"pyproject.toml": PYPROJECT,
|
|
665
|
+
"src/probe/__init__.py": "def one():\n return 1\n",
|
|
666
|
+
"tests/test_probe.py": "from probe import one\n\n\ndef test_one():\n assert one() == 1\n"
|
|
667
|
+
},
|
|
668
|
+
probe: "python3 -m pytest -q -p no:cacheprovider",
|
|
669
|
+
hint: "The Python tasks need python3 with pytest 7 or later. Install it, for example with `python3 -m pip install --user pytest`."
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
var PROBE_TIMEOUT_MS = 12e4;
|
|
673
|
+
var PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
674
|
+
async function checkToolchains(ids) {
|
|
675
|
+
return Promise.all(
|
|
676
|
+
[...new Set(ids)].map(async (id) => {
|
|
677
|
+
const toolchain = TOOLCHAINS[id];
|
|
678
|
+
const r = await inScratch(toolchain, toolchain.probe, PROBE_TIMEOUT_MS);
|
|
679
|
+
return { id, ...r };
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
async function prepareToolchain(id) {
|
|
684
|
+
const toolchain = TOOLCHAINS[id];
|
|
685
|
+
if (toolchain.prepare === void 0) return { ok: false, output: toolchain.hint };
|
|
686
|
+
return inScratch(toolchain, toolchain.prepare, PREPARE_TIMEOUT_MS);
|
|
687
|
+
}
|
|
688
|
+
function toolchainId(name) {
|
|
689
|
+
const aliases = {
|
|
690
|
+
java: "maven",
|
|
691
|
+
maven: "maven",
|
|
692
|
+
python: "pytest",
|
|
693
|
+
pytest: "pytest"
|
|
694
|
+
};
|
|
695
|
+
return aliases[name.toLowerCase()];
|
|
696
|
+
}
|
|
697
|
+
async function inScratch(toolchain, command, timeoutMs) {
|
|
698
|
+
const root = await mkdtemp2(join5(tmpdir2(), `garuda-toolchain-${toolchain.id}-`));
|
|
699
|
+
try {
|
|
700
|
+
await writeFiles(root, toolchain.files);
|
|
701
|
+
const r = await new HostExecutor().run(command, {
|
|
702
|
+
root,
|
|
703
|
+
sandbox: false,
|
|
704
|
+
writePaths: [root],
|
|
705
|
+
denyWritePaths: [],
|
|
706
|
+
denyReadPaths: [],
|
|
707
|
+
network: true,
|
|
708
|
+
envAllowlist: [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV],
|
|
709
|
+
timeoutMs,
|
|
710
|
+
maxOutputBytes: 4e3
|
|
711
|
+
});
|
|
712
|
+
const output = `${r.stdout.text}${r.stderr.text}`.trim();
|
|
713
|
+
return { ok: r.exitCode === 0 && !r.timedOut, output };
|
|
714
|
+
} catch (error) {
|
|
715
|
+
return { ok: false, output: error.message };
|
|
716
|
+
} finally {
|
|
717
|
+
await rm2(root, { recursive: true, force: true });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/evals/repoTasks.ts
|
|
722
|
+
var REPO_SUITE_FILE = join6(".garuda", "evals", "repo-suite.json");
|
|
723
|
+
var DEFAULT_COMMITS = 200;
|
|
724
|
+
var DEFAULT_MAX_TASKS = 30;
|
|
725
|
+
var MAX_TASK_FILES = 10;
|
|
726
|
+
var BUILD_TEST_TIMEOUT_MS = 5 * 6e4;
|
|
727
|
+
var testFile = z.object({ path: z.string(), content: z.string().nullable() });
|
|
728
|
+
var taskSchema = z.object({
|
|
729
|
+
id: z.string(),
|
|
730
|
+
sha: z.string(),
|
|
731
|
+
base: z.string(),
|
|
732
|
+
subject: z.string(),
|
|
733
|
+
prompt: z.string(),
|
|
734
|
+
tests: z.array(testFile),
|
|
735
|
+
/** The check: the test command for this task's test files. Absent: the suite's test command. */
|
|
736
|
+
check: z.string().optional()
|
|
737
|
+
});
|
|
738
|
+
var suiteSchema = z.object({
|
|
739
|
+
version: z.literal(1),
|
|
740
|
+
root: z.string(),
|
|
741
|
+
head: z.string(),
|
|
742
|
+
createdAt: z.string(),
|
|
743
|
+
testCommand: z.string(),
|
|
744
|
+
tasks: z.array(taskSchema)
|
|
745
|
+
});
|
|
746
|
+
var DOC_PATH = /\.(md|mdx|rst|txt|adoc)$|(^|\/)docs?\//i;
|
|
747
|
+
var PACKAGE_JSON = /(^|\/)package\.json$/;
|
|
748
|
+
function skipReason(commit) {
|
|
749
|
+
const { files } = commit;
|
|
750
|
+
if (files.length === 0) return "no files";
|
|
751
|
+
const code = files.filter((f) => !DOC_PATH.test(f.path));
|
|
752
|
+
if (code.length > MAX_TASK_FILES) return `more than ${MAX_TASK_FILES} files`;
|
|
753
|
+
if (files.some((f) => f.added === void 0)) return "a binary file";
|
|
754
|
+
if (files.some((f) => MANIFEST.test(f.path) && !PACKAGE_JSON.test(f.path))) {
|
|
755
|
+
return "dependencies changed";
|
|
756
|
+
}
|
|
757
|
+
const tests = code.filter((f) => TEST_PATH.test(f.path));
|
|
758
|
+
if (tests.length === 0) return "no test change";
|
|
759
|
+
if (tests.length === code.length) return "only tests changed";
|
|
760
|
+
return void 0;
|
|
761
|
+
}
|
|
762
|
+
function dependencyChange(diff) {
|
|
763
|
+
for (const line of diff.split("\n")) {
|
|
764
|
+
if (!/^[+-]/.test(line) || /^(\+\+\+|---)/.test(line)) continue;
|
|
765
|
+
if (/[dD]ependencies"/.test(line)) return true;
|
|
766
|
+
const entry = /^[+-]\s*"([^"]+)":\s*"[~^<>=]*\d/.exec(line);
|
|
767
|
+
if (entry !== null && entry[1] !== "version") return true;
|
|
768
|
+
}
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
function scopedTestCommand(command, files) {
|
|
772
|
+
const list = files.map(shellWord).join(" ");
|
|
773
|
+
if (command.includes("{files}")) return command.replaceAll("{files}", list);
|
|
774
|
+
if (files.length === 0) return command;
|
|
775
|
+
const c = command.trim();
|
|
776
|
+
if (/^npm (run )?test$/.test(c)) return `${c} -- ${list}`;
|
|
777
|
+
if (/^(pnpm|yarn)( run)? test$/.test(c) || /^node --test$/.test(c) || /^(npx |pnpm exec )?(vitest|jest)( run)?$/.test(c) || /-m pytest( -q)?$|^pytest( -q)?$/.test(c)) {
|
|
778
|
+
return `${c} ${list}`;
|
|
779
|
+
}
|
|
780
|
+
return c;
|
|
781
|
+
}
|
|
782
|
+
function repoPrompt(commit, tests) {
|
|
783
|
+
return [
|
|
784
|
+
commit.subject.trim(),
|
|
785
|
+
...commit.body.trim() === "" ? [] : ["", commit.body.trim()],
|
|
786
|
+
"",
|
|
787
|
+
`The tests for this change are already in the project: ${tests.join(", ")}. Make the change so that they pass. Do not change these test files.`
|
|
788
|
+
].join("\n");
|
|
789
|
+
}
|
|
790
|
+
async function recentCommits(executor, root, options = {}) {
|
|
791
|
+
const log = await git(executor, root, [
|
|
792
|
+
"log",
|
|
793
|
+
"--no-merges",
|
|
794
|
+
"--format=%H%x00%s%x00%b%x1e",
|
|
795
|
+
`-n${options.commits ?? DEFAULT_COMMITS}`,
|
|
796
|
+
...options.since === void 0 ? [] : [`--since=${options.since}`],
|
|
797
|
+
"HEAD"
|
|
798
|
+
]);
|
|
799
|
+
const out = [];
|
|
800
|
+
for (const entry of log.stdout.split("")) {
|
|
801
|
+
const [sha = "", subject = "", body = ""] = entry.replace(/^\n/, "").split("\0");
|
|
802
|
+
if (!/^[0-9a-f]{40,64}$/.test(sha)) continue;
|
|
803
|
+
const stat2 = await git(executor, root, ["show", "--no-renames", "--numstat", "--format=", sha]);
|
|
804
|
+
const files = stat2.stdout.split("\n").map((line) => /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line)).filter((m) => m !== null).map(
|
|
805
|
+
(m) => m[1] === "-" ? { path: m[3] } : { path: m[3], added: Number(m[1]), removed: Number(m[2]) }
|
|
806
|
+
);
|
|
807
|
+
out.push({ sha, subject, body, files });
|
|
808
|
+
}
|
|
809
|
+
return out;
|
|
810
|
+
}
|
|
811
|
+
async function taskWorktree(executor, root, commit) {
|
|
812
|
+
const dir = realpathSync(mkdtempSync(join6(tmpdir3(), "garuda-repo-task-")));
|
|
813
|
+
await git(executor, root, ["worktree", "add", "--detach", "--quiet", dir, commit]);
|
|
814
|
+
for (const name of LINK_CANDIDATES) {
|
|
815
|
+
try {
|
|
816
|
+
await symlink(join6(root, name), join6(dir, name), "dir");
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return dir;
|
|
821
|
+
}
|
|
822
|
+
async function isolatedTaskCopy(executor, root, commit) {
|
|
823
|
+
const dir = realpathSync(mkdtempSync(join6(tmpdir3(), "garuda-repo-task-")));
|
|
824
|
+
const indexDir = mkdtempSync(join6(tmpdir3(), "garuda-repo-index-"));
|
|
825
|
+
try {
|
|
826
|
+
const env = { GIT_INDEX_FILE: join6(indexDir, "index") };
|
|
827
|
+
await git(executor, root, ["read-tree", commit], { env });
|
|
828
|
+
await git(executor, root, ["checkout-index", "--all", "--force", `--prefix=${dir}/`], { env });
|
|
829
|
+
} finally {
|
|
830
|
+
await rm3(indexDir, { recursive: true, force: true });
|
|
831
|
+
}
|
|
832
|
+
await git(executor, dir, ["init", "--quiet"]);
|
|
833
|
+
await git(executor, dir, ["add", "--all"]);
|
|
834
|
+
await git(executor, dir, [
|
|
835
|
+
"-c",
|
|
836
|
+
"user.name=Garuda eval",
|
|
837
|
+
"-c",
|
|
838
|
+
"user.email=eval@garuda.invalid",
|
|
839
|
+
"commit",
|
|
840
|
+
"--quiet",
|
|
841
|
+
"--no-verify",
|
|
842
|
+
"--message",
|
|
843
|
+
"The task's start"
|
|
844
|
+
]);
|
|
845
|
+
for (const name of LINK_CANDIDATES) {
|
|
846
|
+
try {
|
|
847
|
+
await symlink(join6(root, name), join6(dir, name), "dir");
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return dir;
|
|
852
|
+
}
|
|
853
|
+
function repoTaskReadDenials(root, home) {
|
|
854
|
+
const links = LINK_CANDIDATES.map((name) => join6(root, name));
|
|
855
|
+
const inHome = root === home || root.startsWith(`${home}${sep}`);
|
|
856
|
+
return [
|
|
857
|
+
...inHome ? denyAround(home, [...links, join6(home, "Library")], true) : [],
|
|
858
|
+
...inHome ? [] : denyAround(root, links)
|
|
859
|
+
];
|
|
860
|
+
}
|
|
861
|
+
var NODE_MODULES_CACHES = [".vite-temp", ".vite", ".cache"];
|
|
862
|
+
function repoTaskWritePaths(root) {
|
|
863
|
+
let modules;
|
|
864
|
+
try {
|
|
865
|
+
modules = realpathSync(join6(root, "node_modules"));
|
|
866
|
+
} catch {
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
const out = [];
|
|
870
|
+
for (const name of NODE_MODULES_CACHES) {
|
|
871
|
+
const path = join6(modules, name);
|
|
872
|
+
try {
|
|
873
|
+
mkdirSync2(path, { recursive: true });
|
|
874
|
+
out.push(path);
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return out;
|
|
879
|
+
}
|
|
880
|
+
async function removeTaskWorktree(executor, root, dir) {
|
|
881
|
+
await git(executor, root, ["worktree", "remove", "--force", dir], { check: false });
|
|
882
|
+
await rm3(dir, { recursive: true, force: true });
|
|
883
|
+
await git(executor, root, ["worktree", "prune"], { check: false });
|
|
884
|
+
}
|
|
885
|
+
async function applyTests(dir, tests) {
|
|
886
|
+
for (const t of tests) {
|
|
887
|
+
const file = join6(dir, t.path);
|
|
888
|
+
if (t.content === null) await rm3(file, { force: true });
|
|
889
|
+
else {
|
|
890
|
+
await mkdir(dirname2(file), { recursive: true });
|
|
891
|
+
await writeFile3(file, t.content);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async function runTests(executor, dir, command) {
|
|
896
|
+
const result = await executor.run(command, {
|
|
897
|
+
root: dir,
|
|
898
|
+
sandbox: false,
|
|
899
|
+
writePaths: [dir],
|
|
900
|
+
denyWritePaths: [],
|
|
901
|
+
denyReadPaths: [],
|
|
902
|
+
network: false,
|
|
903
|
+
envAllowlist: [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV],
|
|
904
|
+
timeoutMs: BUILD_TEST_TIMEOUT_MS,
|
|
905
|
+
maxOutputBytes: 2e4
|
|
906
|
+
});
|
|
907
|
+
const output = `${result.stdout.text}${result.stderr.text}`;
|
|
908
|
+
return {
|
|
909
|
+
ok: result.exitCode === 0 && !result.timedOut,
|
|
910
|
+
tail: result.timedOut ? "(stopped: too long)" : tail(output, 15)
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
async function buildRepoSuite(executor, root, options) {
|
|
914
|
+
const say = options.onProgress ?? (() => {
|
|
915
|
+
});
|
|
916
|
+
const head = (await git(executor, root, ["rev-parse", "HEAD"])).stdout.trim();
|
|
917
|
+
const commits = await recentCommits(executor, root, {
|
|
918
|
+
...options.commits === void 0 ? {} : { commits: options.commits },
|
|
919
|
+
...options.since === void 0 ? {} : { since: options.since }
|
|
920
|
+
});
|
|
921
|
+
const skipped = {};
|
|
922
|
+
const skip = (why) => {
|
|
923
|
+
skipped[why] = (skipped[why] ?? 0) + 1;
|
|
924
|
+
};
|
|
925
|
+
const tasks = [];
|
|
926
|
+
let checked = 0;
|
|
927
|
+
let failure;
|
|
928
|
+
let evalFailure;
|
|
929
|
+
const max = options.maxTasks ?? DEFAULT_MAX_TASKS;
|
|
930
|
+
for (const commit of commits) {
|
|
931
|
+
if (tasks.length >= max) break;
|
|
932
|
+
const why = skipReason(commit);
|
|
933
|
+
if (why !== void 0) {
|
|
934
|
+
skip(why);
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
const parent = await git(
|
|
938
|
+
executor,
|
|
939
|
+
root,
|
|
940
|
+
["rev-parse", "--verify", "--quiet", `${commit.sha}^`],
|
|
941
|
+
{
|
|
942
|
+
check: false
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
if (parent.exitCode !== 0) {
|
|
946
|
+
skip("the first commit (no parent)");
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (commit.files.some((f) => PACKAGE_JSON.test(f.path))) {
|
|
950
|
+
const diff = await git(
|
|
951
|
+
executor,
|
|
952
|
+
root,
|
|
953
|
+
["show", "--format=", commit.sha, "--", "package.json"],
|
|
954
|
+
{
|
|
955
|
+
check: false
|
|
956
|
+
}
|
|
957
|
+
);
|
|
958
|
+
if (dependencyChange(diff.stdout)) {
|
|
959
|
+
skip("dependencies changed");
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const base = parent.stdout.trim();
|
|
964
|
+
const testPaths = commit.files.filter((f) => TEST_PATH.test(f.path) && !DOC_PATH.test(f.path)).map((f) => f.path);
|
|
965
|
+
const tests = [];
|
|
966
|
+
for (const path of testPaths) {
|
|
967
|
+
const shown = await git(executor, root, ["show", `${commit.sha}:${path}`], { check: false });
|
|
968
|
+
tests.push({ path, content: shown.exitCode === 0 ? shown.stdout : null });
|
|
969
|
+
}
|
|
970
|
+
const runnable = tests.filter((t) => t.content !== null).map((t) => t.path);
|
|
971
|
+
if (runnable.length === 0) {
|
|
972
|
+
skip("only deletes tests");
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const check = scopedTestCommand(options.testCommand, runnable);
|
|
976
|
+
checked++;
|
|
977
|
+
say(`checking ${commit.sha.slice(0, 7)} ${commit.subject}`);
|
|
978
|
+
const dir = await taskWorktree(executor, root, base);
|
|
979
|
+
try {
|
|
980
|
+
await applyTests(dir, tests);
|
|
981
|
+
if ((await runTests(executor, dir, check)).ok) {
|
|
982
|
+
skip("the tests already pass at the parent");
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
await git(executor, dir, ["checkout", "--quiet", "--", "."], { check: false });
|
|
986
|
+
await git(executor, dir, cleanArgs(), { check: false });
|
|
987
|
+
await git(executor, dir, ["checkout", "--quiet", "--detach", commit.sha]);
|
|
988
|
+
const atCommit = await runTests(executor, dir, check);
|
|
989
|
+
if (!atCommit.ok) {
|
|
990
|
+
skip("the tests fail at the commit");
|
|
991
|
+
failure ??= { sha: commit.sha, command: check, tail: atCommit.tail };
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
} finally {
|
|
995
|
+
await removeTaskWorktree(executor, root, dir);
|
|
996
|
+
}
|
|
997
|
+
if (options.evalCheck !== void 0) {
|
|
998
|
+
const inEval = await options.evalCheck(commit.sha, check);
|
|
999
|
+
if (!inEval.ok) {
|
|
1000
|
+
skip("the tests fail as an eval runs them (a task copy, in the sandbox)");
|
|
1001
|
+
evalFailure ??= { sha: commit.sha, command: check, tail: inEval.tail };
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
tasks.push({
|
|
1006
|
+
id: `repo-${commit.sha.slice(0, 7)}`,
|
|
1007
|
+
sha: commit.sha,
|
|
1008
|
+
base,
|
|
1009
|
+
subject: commit.subject,
|
|
1010
|
+
prompt: repoPrompt(commit, testPaths),
|
|
1011
|
+
tests,
|
|
1012
|
+
check
|
|
1013
|
+
});
|
|
1014
|
+
say(`kept ${commit.sha.slice(0, 7)} (${tasks.length}/${max})`);
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
suite: {
|
|
1018
|
+
version: 1,
|
|
1019
|
+
root,
|
|
1020
|
+
head,
|
|
1021
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1022
|
+
testCommand: options.testCommand,
|
|
1023
|
+
tasks
|
|
1024
|
+
},
|
|
1025
|
+
checked,
|
|
1026
|
+
skipped,
|
|
1027
|
+
...failure === void 0 ? {} : { failure },
|
|
1028
|
+
...evalFailure === void 0 ? {} : { evalFailure }
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
async function saveRepoSuite(suite) {
|
|
1032
|
+
const file = join6(suite.root, REPO_SUITE_FILE);
|
|
1033
|
+
await mkdir(dirname2(file), { recursive: true });
|
|
1034
|
+
await writeFile3(file, `${JSON.stringify(suite, null, 2)}
|
|
1035
|
+
`, { mode: 384 });
|
|
1036
|
+
return file;
|
|
1037
|
+
}
|
|
1038
|
+
async function loadRepoSuite(root) {
|
|
1039
|
+
let text;
|
|
1040
|
+
try {
|
|
1041
|
+
text = await readFile3(join6(root, REPO_SUITE_FILE), "utf8");
|
|
1042
|
+
} catch {
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
const parsed = suiteSchema.safeParse(JSON.parse(text));
|
|
1046
|
+
if (!parsed.success) throw new Error(`${REPO_SUITE_FILE}: ${z.prettifyError(parsed.error)}`);
|
|
1047
|
+
return parsed.data;
|
|
1048
|
+
}
|
|
1049
|
+
function repoEvalTasks(suite) {
|
|
1050
|
+
return suite.tasks.map((t) => ({
|
|
1051
|
+
id: t.id,
|
|
1052
|
+
title: t.subject,
|
|
1053
|
+
prompt: t.prompt,
|
|
1054
|
+
files: {},
|
|
1055
|
+
check: t.check ?? suite.testCommand,
|
|
1056
|
+
protect: t.tests.filter((f) => f.content !== null).map((f) => f.path),
|
|
1057
|
+
solution: {},
|
|
1058
|
+
repo: { root: suite.root, base: t.base, sha: t.sha, tests: t.tests }
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/evals/runner.ts
|
|
1063
|
+
var EVAL_DENY_RULES = [
|
|
1064
|
+
"bash(rm -rf*)",
|
|
1065
|
+
"bash(sudo*)",
|
|
1066
|
+
"bash(git push*)",
|
|
1067
|
+
"bash(curl*)",
|
|
1068
|
+
"bash(wget*)"
|
|
1069
|
+
];
|
|
1070
|
+
var REPO_CHECK_TIMEOUT_MS = 5 * 6e4;
|
|
1071
|
+
var EVAL_TASK_TIMEOUT_MS = 10 * 6e4;
|
|
1072
|
+
var PROVIDER_STALL_MS = 12e4;
|
|
1073
|
+
var PROVIDER_STALLED = "The model's API stalled:";
|
|
1074
|
+
var MCP_PREPARE_TIMEOUT_MS = 12e4;
|
|
1075
|
+
var ModelWatch = class {
|
|
1076
|
+
steps = 0;
|
|
1077
|
+
/** Tool calls that started and have no result yet. */
|
|
1078
|
+
running = 0;
|
|
1079
|
+
/** The last response asked for tools that have not started yet. */
|
|
1080
|
+
toolsNext = false;
|
|
1081
|
+
lastAt = Date.now();
|
|
1082
|
+
retried = false;
|
|
1083
|
+
see(event, now = Date.now()) {
|
|
1084
|
+
switch (event.type) {
|
|
1085
|
+
case "model_retry":
|
|
1086
|
+
this.retried = true;
|
|
1087
|
+
return;
|
|
1088
|
+
case "tool_call":
|
|
1089
|
+
this.running++;
|
|
1090
|
+
this.toolsNext = false;
|
|
1091
|
+
break;
|
|
1092
|
+
case "tool_result":
|
|
1093
|
+
this.running = Math.max(0, this.running - 1);
|
|
1094
|
+
break;
|
|
1095
|
+
case "step_end":
|
|
1096
|
+
this.steps = event.step;
|
|
1097
|
+
this.toolsNext = event.response.content.some((b) => b.type === "tool_use");
|
|
1098
|
+
this.retried = false;
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
this.lastAt = now;
|
|
1102
|
+
}
|
|
1103
|
+
/** A model request is open: no tool runs or is about to. */
|
|
1104
|
+
get waiting() {
|
|
1105
|
+
return this.running === 0 && !this.toolsNext;
|
|
1106
|
+
}
|
|
1107
|
+
/** Why the stop is the provider's, or undefined: a tool was running, or the model was sending. */
|
|
1108
|
+
stall(now, quietMs) {
|
|
1109
|
+
if (!this.waiting) return void 0;
|
|
1110
|
+
const quiet = Math.round((now - this.lastAt) / 1e3);
|
|
1111
|
+
if (this.retried) return `its stream broke and was retried, and it sent nothing for ${quiet} s`;
|
|
1112
|
+
if (now - this.lastAt >= quietMs) return `it sent nothing for ${quiet} s`;
|
|
1113
|
+
return void 0;
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
function repoTaskSandbox(repoRoot, home) {
|
|
1117
|
+
return {
|
|
1118
|
+
denyRead: repoTaskReadDenials(repoRoot, home),
|
|
1119
|
+
writePaths: repoTaskWritePaths(repoRoot)
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
async function checkAsEval(repoRoot, sha, check, options = {}) {
|
|
1123
|
+
const host = new HostExecutor();
|
|
1124
|
+
const root = await isolatedTaskCopy(host, repoRoot, sha);
|
|
1125
|
+
try {
|
|
1126
|
+
const runtime = await Runtime.create({
|
|
1127
|
+
root,
|
|
1128
|
+
modelId: "none",
|
|
1129
|
+
model: async () => {
|
|
1130
|
+
throw new Error("checkAsEval asks no model.");
|
|
1131
|
+
},
|
|
1132
|
+
approver: new AutoApprover("once"),
|
|
1133
|
+
store: new FileSessionStore(root),
|
|
1134
|
+
settings: parseSettings({
|
|
1135
|
+
permissions: { deny: EVAL_DENY_RULES },
|
|
1136
|
+
web: { enabled: false },
|
|
1137
|
+
sandbox: repoTaskSandbox(repoRoot, options.home ?? homedir()),
|
|
1138
|
+
...options.executor === void 0 ? {} : { executor: options.executor }
|
|
1139
|
+
}),
|
|
1140
|
+
mcp: false,
|
|
1141
|
+
hooks: false,
|
|
1142
|
+
commands: false,
|
|
1143
|
+
...options.policy === void 0 ? {} : { policy: options.policy }
|
|
1144
|
+
});
|
|
1145
|
+
try {
|
|
1146
|
+
const result = await runCheck(
|
|
1147
|
+
root,
|
|
1148
|
+
check,
|
|
1149
|
+
REPO_CHECK_TIMEOUT_MS,
|
|
1150
|
+
runtime.executor.isolation === "none" ? void 0 : { executor: runtime.executor, policy: runtime.execPolicy(REPO_CHECK_TIMEOUT_MS) }
|
|
1151
|
+
);
|
|
1152
|
+
return { ok: result.ok, tail: result.output.split("\n").slice(-15).join("\n") };
|
|
1153
|
+
} finally {
|
|
1154
|
+
await runtime.close();
|
|
1155
|
+
}
|
|
1156
|
+
} finally {
|
|
1157
|
+
await rm4(root, { recursive: true, force: true });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async function runEvalTask(task, options) {
|
|
1161
|
+
const started = Date.now();
|
|
1162
|
+
const host = new HostExecutor();
|
|
1163
|
+
let root;
|
|
1164
|
+
if (task.repo === void 0) {
|
|
1165
|
+
root = realpathSync2(await mkdtemp3(join7(tmpdir4(), `garuda-eval-${task.id}-`)));
|
|
1166
|
+
await writeFiles(root, task.files);
|
|
1167
|
+
} else {
|
|
1168
|
+
root = await isolatedTaskCopy(host, task.repo.root, task.repo.base);
|
|
1169
|
+
await applyTests(root, task.repo.tests);
|
|
1170
|
+
}
|
|
1171
|
+
if (options.format !== void 0) await formatProject(root, options.format.biome);
|
|
1172
|
+
const protect = task.protect ?? Object.keys(task.files).filter(isProtectedPath);
|
|
1173
|
+
const before = await snapshot(root, protect);
|
|
1174
|
+
const settings = parseSettings({
|
|
1175
|
+
// With an allowlist, a host outside it is denied: nobody answers questions in an eval.
|
|
1176
|
+
permissions: {
|
|
1177
|
+
deny: options.network === void 0 ? EVAL_DENY_RULES : [...EVAL_DENY_RULES, "network"]
|
|
1178
|
+
},
|
|
1179
|
+
...options.network === void 0 ? {} : { network: { allow: options.network } },
|
|
1180
|
+
// Evals must not depend on the internet.
|
|
1181
|
+
web: { enabled: false },
|
|
1182
|
+
// 0.18.1: a repo task's commands cannot read the project's own folder or the rest of the
|
|
1183
|
+
// home folder: the checkout, and copies of it, hold the commit that the task asks for.
|
|
1184
|
+
...task.repo === void 0 ? {} : { sandbox: repoTaskSandbox(task.repo.root, options.home ?? homedir()) },
|
|
1185
|
+
...options.executor === void 0 ? {} : { executor: options.executor },
|
|
1186
|
+
...options.codeIndex === void 0 ? {} : { codeIndex: options.codeIndex },
|
|
1187
|
+
...options.maxSteps === void 0 ? {} : { limits: { maxSteps: options.maxSteps } },
|
|
1188
|
+
...options.subagents === void 0 ? {} : { subagents: { enabled: options.subagents } },
|
|
1189
|
+
...options.todo === void 0 ? {} : { todo: { enabled: options.todo } },
|
|
1190
|
+
...options.format === void 0 ? {} : {
|
|
1191
|
+
formatters: {
|
|
1192
|
+
enabled: options.format.on,
|
|
1193
|
+
commands: {
|
|
1194
|
+
biome: {
|
|
1195
|
+
extensions: ["js", "jsx", "mjs", "cjs", "ts", "tsx", "json"],
|
|
1196
|
+
command: [options.format.biome, "format", "--write", "$FILE"]
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
...options.keepThinking === void 0 ? {} : { thinking: { keepBlocks: options.keepThinking } }
|
|
1202
|
+
});
|
|
1203
|
+
const store = new FileSessionStore(root);
|
|
1204
|
+
const watch = new ModelWatch();
|
|
1205
|
+
let mcpCalls = 0;
|
|
1206
|
+
const tools = [];
|
|
1207
|
+
const createRuntime = (prepare2) => Runtime.create({
|
|
1208
|
+
root,
|
|
1209
|
+
modelId: options.modelId,
|
|
1210
|
+
model: async () => options.model(),
|
|
1211
|
+
...options.modelInfo === void 0 ? {} : { modelInfo: options.modelInfo },
|
|
1212
|
+
...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens },
|
|
1213
|
+
...options.subagentModel === void 0 ? {} : { subagentModel: options.subagentModel },
|
|
1214
|
+
approver: new AutoApprover("once"),
|
|
1215
|
+
store,
|
|
1216
|
+
settings,
|
|
1217
|
+
// The user's own MCP servers must not change eval results; `--mcp` allows one by name.
|
|
1218
|
+
mcp: options.mcp === void 0 || prepare2 ? false : { home: options.mcp.home, only: options.mcp.server },
|
|
1219
|
+
hooks: false,
|
|
1220
|
+
...options.addendum === void 0 ? {} : { addendum: options.addendum },
|
|
1221
|
+
...options.policy === void 0 ? {} : { policy: options.policy },
|
|
1222
|
+
commands: false,
|
|
1223
|
+
// The list comes from the eval command line: the user typed it.
|
|
1224
|
+
...options.network === void 0 ? {} : { network: { approved: true } },
|
|
1225
|
+
...options.lsp === void 0 ? {} : { lsp: { enabled: options.lsp } },
|
|
1226
|
+
onEvent: (event) => {
|
|
1227
|
+
if (event.type === "tool_call") {
|
|
1228
|
+
tools.push(event.call.name);
|
|
1229
|
+
if (event.call.name.startsWith("mcp__")) mcpCalls++;
|
|
1230
|
+
}
|
|
1231
|
+
watch.see(event);
|
|
1232
|
+
options.onEvent?.(task.id, event);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
let prepareFailure;
|
|
1236
|
+
const prepare = options.mcp?.prepare;
|
|
1237
|
+
if (prepare !== void 0) {
|
|
1238
|
+
const prep = await createRuntime(true);
|
|
1239
|
+
try {
|
|
1240
|
+
const prepared = await runCheck(
|
|
1241
|
+
root,
|
|
1242
|
+
prepare,
|
|
1243
|
+
MCP_PREPARE_TIMEOUT_MS,
|
|
1244
|
+
prep.executor.isolation === "none" ? void 0 : { executor: prep.executor, policy: prep.execPolicy(MCP_PREPARE_TIMEOUT_MS) }
|
|
1245
|
+
);
|
|
1246
|
+
if (!prepared.ok) prepareFailure = `${MCP_PREPARE_FAILED}
|
|
1247
|
+
${prepared.output}`;
|
|
1248
|
+
} finally {
|
|
1249
|
+
await prep.close();
|
|
1250
|
+
prep.executor.shutdown();
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const runtime = await createRuntime(false);
|
|
1254
|
+
const result = {
|
|
1255
|
+
id: task.id,
|
|
1256
|
+
title: task.title,
|
|
1257
|
+
passed: false,
|
|
1258
|
+
stopReason: "error",
|
|
1259
|
+
steps: 0,
|
|
1260
|
+
tokens: 0,
|
|
1261
|
+
costUsd: void 0,
|
|
1262
|
+
durationMs: 0
|
|
1263
|
+
};
|
|
1264
|
+
if (prepareFailure !== void 0) result.reason = prepareFailure;
|
|
1265
|
+
const start = await treeState(root);
|
|
1266
|
+
const limitMs = options.taskTimeoutMs ?? EVAL_TASK_TIMEOUT_MS;
|
|
1267
|
+
const timeout = AbortSignal.timeout(limitMs);
|
|
1268
|
+
try {
|
|
1269
|
+
if (result.reason !== void 0) {
|
|
1270
|
+
} else if (options.agent !== void 0) {
|
|
1271
|
+
await runtime.prepareCommands(timeout);
|
|
1272
|
+
const { result: exec, outcome } = await runClaudeCode(
|
|
1273
|
+
runtime.executor,
|
|
1274
|
+
runtime.execPolicy(limitMs),
|
|
1275
|
+
options.agent,
|
|
1276
|
+
task.prompt,
|
|
1277
|
+
options.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
1278
|
+
timeout
|
|
1279
|
+
);
|
|
1280
|
+
result.stopReason = outcome.stopReason;
|
|
1281
|
+
result.steps = outcome.steps;
|
|
1282
|
+
result.tokens = outcome.tokens;
|
|
1283
|
+
result.cacheReadTokens = outcome.cacheReadTokens;
|
|
1284
|
+
result.costUsd = outcome.costUsd;
|
|
1285
|
+
if (outcome.reason !== void 0) result.reason = outcome.reason;
|
|
1286
|
+
if (options.outDir !== void 0) {
|
|
1287
|
+
mkdirSync3(options.outDir, { recursive: true });
|
|
1288
|
+
result.sessionFile = await saveUnique(
|
|
1289
|
+
options.outDir,
|
|
1290
|
+
task.id,
|
|
1291
|
+
".claude.json",
|
|
1292
|
+
(path) => writeFile4(path, exec.stdout.text, { flag: "wx" })
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
const run = await runtime.runTurn(task.prompt, timeout);
|
|
1297
|
+
result.stopReason = run.stopReason;
|
|
1298
|
+
result.steps = run.steps;
|
|
1299
|
+
}
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
runtime.recordStop("error");
|
|
1302
|
+
result.steps = watch.steps;
|
|
1303
|
+
const stall = timeout.aborted ? watch.stall(Date.now(), options.providerStallMs ?? PROVIDER_STALL_MS) : void 0;
|
|
1304
|
+
if (stall !== void 0) {
|
|
1305
|
+
result.stopReason = "error";
|
|
1306
|
+
result.reason = `${PROVIDER_STALLED} ${stall} when the task hit its ${limitMs / 6e4}-minute limit.`;
|
|
1307
|
+
} else if (timeout.aborted) {
|
|
1308
|
+
result.stopReason = "timeout";
|
|
1309
|
+
result.reason = `The task took longer than ${limitMs / 6e4} minutes.`;
|
|
1310
|
+
} else {
|
|
1311
|
+
result.reason = `The run failed: ${error.message}`;
|
|
1312
|
+
}
|
|
1313
|
+
} finally {
|
|
1314
|
+
if (tools.length > 0) result.tools = tools;
|
|
1315
|
+
if (options.mcp !== void 0) {
|
|
1316
|
+
result.mcpCalls = mcpCalls;
|
|
1317
|
+
const status = runtime.mcpStatus().find((m) => m.name === options.mcp?.server);
|
|
1318
|
+
if (result.reason === void 0 && status !== void 0 && status.state === "failed") {
|
|
1319
|
+
result.stopReason = "error";
|
|
1320
|
+
result.reason = `${MCP_NOT_STARTED} "${options.mcp.server}": ${status.message ?? "no reason given"}`;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
await runtime.close();
|
|
1324
|
+
runtime.executor.shutdown();
|
|
1325
|
+
}
|
|
1326
|
+
const session = runtime.session;
|
|
1327
|
+
if (session !== void 0 && options.agent === void 0) {
|
|
1328
|
+
result.tokens = totalTokens(session.usage);
|
|
1329
|
+
if (session.unreportedTokens > 0) result.unreportedTokens = session.unreportedTokens;
|
|
1330
|
+
result.costUsd = session.costUsd;
|
|
1331
|
+
if (isCostIncomplete(session)) result.costIncomplete = true;
|
|
1332
|
+
result.cacheReadTokens = session.usage.cacheReadTokens;
|
|
1333
|
+
if (options.outDir !== void 0) {
|
|
1334
|
+
mkdirSync3(options.outDir, { recursive: true });
|
|
1335
|
+
const from = store.path(session.id);
|
|
1336
|
+
result.sessionFile = await saveUnique(
|
|
1337
|
+
options.outDir,
|
|
1338
|
+
task.id,
|
|
1339
|
+
".jsonl",
|
|
1340
|
+
(path) => copyFile(from, path, fsConstants.COPYFILE_EXCL)
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
result.changed = changedPaths(start, await treeState(root));
|
|
1345
|
+
const expected = await expectedFiles(task, host);
|
|
1346
|
+
if (expected !== void 0) {
|
|
1347
|
+
result.unexpected = unexpectedPaths(result.changed, expected, task.allowChanges);
|
|
1348
|
+
}
|
|
1349
|
+
if (result.reason === void 0) {
|
|
1350
|
+
const changed = await changedFiles(root, before);
|
|
1351
|
+
const checkMs = task.repo === void 0 ? 12e4 : REPO_CHECK_TIMEOUT_MS;
|
|
1352
|
+
const check = await runCheck(
|
|
1353
|
+
root,
|
|
1354
|
+
task.check,
|
|
1355
|
+
checkMs,
|
|
1356
|
+
runtime.executor.isolation === "none" ? void 0 : { executor: runtime.executor, policy: runtime.execPolicy(checkMs) }
|
|
1357
|
+
);
|
|
1358
|
+
if (changed.length > 0) result.reason = `${PROTECTED_FILES_CHANGED} ${changed.join(", ")}.`;
|
|
1359
|
+
else if (!check.ok) result.reason = `The check failed:
|
|
1360
|
+
${check.output}`;
|
|
1361
|
+
else result.passed = true;
|
|
1362
|
+
}
|
|
1363
|
+
result.durationMs = Date.now() - started;
|
|
1364
|
+
if (!options.keep) {
|
|
1365
|
+
await rm4(root, { recursive: true, force: true });
|
|
1366
|
+
}
|
|
1367
|
+
return result;
|
|
1368
|
+
}
|
|
1369
|
+
async function expectedFiles(task, host) {
|
|
1370
|
+
if (task.repo === void 0) return Object.keys(task.solution);
|
|
1371
|
+
if (task.repo.sha === void 0) return void 0;
|
|
1372
|
+
const r = await git(
|
|
1373
|
+
host,
|
|
1374
|
+
task.repo.root,
|
|
1375
|
+
["diff", "--name-only", task.repo.base, task.repo.sha],
|
|
1376
|
+
{
|
|
1377
|
+
check: false
|
|
1378
|
+
}
|
|
1379
|
+
);
|
|
1380
|
+
if (r.exitCode !== 0) return void 0;
|
|
1381
|
+
const tests = new Set(task.repo.tests.map((t) => t.path));
|
|
1382
|
+
return r.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !tests.has(line));
|
|
1383
|
+
}
|
|
1384
|
+
function isAuthFailure(result) {
|
|
1385
|
+
if (!isErrorRun(result) || result.reason === void 0) return false;
|
|
1386
|
+
return /\b40[13]\b|authentication_error|permission_error|invalid x-api-key|invalid api key/i.test(
|
|
1387
|
+
result.reason
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
function isConnectionFailure(result) {
|
|
1391
|
+
if (!isErrorRun(result) || result.reason === void 0) return false;
|
|
1392
|
+
return /ECONNREFUSED|ENOTFOUND|EAI_AGAIN|connection refused|self[- ]signed certificate|unable to get local issuer certificate|certificate has expired/i.test(
|
|
1393
|
+
result.reason
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
async function saveUnique(dir, id, ext, write) {
|
|
1397
|
+
for (let n = 1; ; n++) {
|
|
1398
|
+
const path = join7(dir, `${id}${n === 1 ? "" : `-${n}`}${ext}`);
|
|
1399
|
+
try {
|
|
1400
|
+
await write(path);
|
|
1401
|
+
return path;
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
if (error.code !== "EEXIST") throw error;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
var PROTECTED_FILES_CHANGED = "The agent changed protected files:";
|
|
1408
|
+
var MCP_PREPARE_FAILED = "The MCP prepare command failed:";
|
|
1409
|
+
var MCP_NOT_STARTED = "The MCP server did not start:";
|
|
1410
|
+
function isMcpFailure(result) {
|
|
1411
|
+
return result.reason !== void 0 && (result.reason.startsWith(MCP_PREPARE_FAILED) || result.reason.startsWith(MCP_NOT_STARTED));
|
|
1412
|
+
}
|
|
1413
|
+
function isProviderStall(result) {
|
|
1414
|
+
return isErrorRun(result) && result.reason?.startsWith(PROVIDER_STALLED) === true;
|
|
1415
|
+
}
|
|
1416
|
+
function isSetupFailure(result) {
|
|
1417
|
+
if (isProviderStall(result)) return false;
|
|
1418
|
+
return isAuthFailure(result) || isConnectionFailure(result) || isMcpFailure(result);
|
|
1419
|
+
}
|
|
1420
|
+
var FAILURE_KINDS = [
|
|
1421
|
+
"check",
|
|
1422
|
+
"protected-files",
|
|
1423
|
+
"step-limit",
|
|
1424
|
+
"token-limit",
|
|
1425
|
+
"repeated-calls",
|
|
1426
|
+
"refusal",
|
|
1427
|
+
"timeout",
|
|
1428
|
+
"provider",
|
|
1429
|
+
"error",
|
|
1430
|
+
"setup"
|
|
1431
|
+
];
|
|
1432
|
+
function failureKind(result) {
|
|
1433
|
+
if (result.passed) return void 0;
|
|
1434
|
+
if (isErrorRun(result)) {
|
|
1435
|
+
if (isProviderStall(result)) return "provider";
|
|
1436
|
+
return isSetupFailure(result) ? "setup" : "error";
|
|
1437
|
+
}
|
|
1438
|
+
switch (result.stopReason) {
|
|
1439
|
+
case "timeout":
|
|
1440
|
+
return "timeout";
|
|
1441
|
+
case "max_steps":
|
|
1442
|
+
return "step-limit";
|
|
1443
|
+
case "token_budget":
|
|
1444
|
+
case "max_tokens":
|
|
1445
|
+
return "token-limit";
|
|
1446
|
+
case "repeated_calls":
|
|
1447
|
+
return "repeated-calls";
|
|
1448
|
+
case "refusal":
|
|
1449
|
+
return "refusal";
|
|
1450
|
+
}
|
|
1451
|
+
if (result.reason?.startsWith(PROTECTED_FILES_CHANGED) === true) return "protected-files";
|
|
1452
|
+
return "check";
|
|
1453
|
+
}
|
|
1454
|
+
function failureSummary(results) {
|
|
1455
|
+
const runs = [];
|
|
1456
|
+
for (const r of results) {
|
|
1457
|
+
const kind = failureKind(r);
|
|
1458
|
+
if (kind === void 0) continue;
|
|
1459
|
+
runs.push({
|
|
1460
|
+
id: r.id,
|
|
1461
|
+
attempt: r.attempt ?? 1,
|
|
1462
|
+
kind,
|
|
1463
|
+
steps: r.steps,
|
|
1464
|
+
tokens: r.tokens,
|
|
1465
|
+
durationMs: r.durationMs
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
const byKind = {};
|
|
1469
|
+
for (const kind of FAILURE_KINDS) {
|
|
1470
|
+
const n = runs.filter((f) => f.kind === kind).length;
|
|
1471
|
+
if (n > 0) byKind[kind] = n;
|
|
1472
|
+
}
|
|
1473
|
+
return { total: runs.length, byKind, runs };
|
|
1474
|
+
}
|
|
1475
|
+
function failureLine(results) {
|
|
1476
|
+
const s = failureSummary(results);
|
|
1477
|
+
if (s.total === 0) return void 0;
|
|
1478
|
+
const parts = FAILURE_KINDS.flatMap((kind) => {
|
|
1479
|
+
const runs = s.runs.filter((f) => f.kind === kind);
|
|
1480
|
+
if (runs.length === 0) return [];
|
|
1481
|
+
const named = runs.slice(0, 3).map((f) => `${f.id} run ${f.attempt}`);
|
|
1482
|
+
if (runs.length > 3) named.push(`${runs.length - 3} more`);
|
|
1483
|
+
return [`${kind} ${runs.length} (${named.join(", ")})`];
|
|
1484
|
+
});
|
|
1485
|
+
return `Failures by kind: ${parts.join(" \xB7 ")}`;
|
|
1486
|
+
}
|
|
1487
|
+
function withStalledRun(again, stalled) {
|
|
1488
|
+
const unreported = (again.unreportedTokens ?? 0) + (stalled.unreportedTokens ?? 0);
|
|
1489
|
+
return {
|
|
1490
|
+
...again,
|
|
1491
|
+
tokens: again.tokens + stalled.tokens,
|
|
1492
|
+
...unreported > 0 ? { unreportedTokens: unreported } : {},
|
|
1493
|
+
costUsd: again.costUsd === void 0 || stalled.costUsd === void 0 ? void 0 : again.costUsd + stalled.costUsd,
|
|
1494
|
+
...again.costIncomplete || stalled.costIncomplete ? { costIncomplete: true } : {},
|
|
1495
|
+
providerStalls: (again.providerStalls ?? 0) + 1
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
async function runEvals(tasks, options, onResult, stop) {
|
|
1499
|
+
const results = new Array(tasks.length);
|
|
1500
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1501
|
+
const attempts = tasks.map((t) => {
|
|
1502
|
+
const n = (seen.get(t.id) ?? 0) + 1;
|
|
1503
|
+
seen.set(t.id, n);
|
|
1504
|
+
return n;
|
|
1505
|
+
});
|
|
1506
|
+
let next = 0;
|
|
1507
|
+
let stopped = false;
|
|
1508
|
+
const worker = async () => {
|
|
1509
|
+
for (let i = next++; i < tasks.length && !stopped; i = next++) {
|
|
1510
|
+
let result = await runEvalTask(tasks[i], options);
|
|
1511
|
+
if (isProviderStall(result)) {
|
|
1512
|
+
options.onEvent?.(result.id, {
|
|
1513
|
+
type: "notice",
|
|
1514
|
+
text: `${result.reason ?? PROVIDER_STALLED} Running it again.`
|
|
1515
|
+
});
|
|
1516
|
+
result = withStalledRun(await runEvalTask(tasks[i], options), result);
|
|
1517
|
+
}
|
|
1518
|
+
result.attempt = attempts[i];
|
|
1519
|
+
results[i] = result;
|
|
1520
|
+
onResult?.(result);
|
|
1521
|
+
if (stop?.(result)) stopped = true;
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
const width = Math.max(1, Math.min(options.parallel ?? 1, tasks.length));
|
|
1525
|
+
await Promise.all(Array.from({ length: width }, worker));
|
|
1526
|
+
return results.filter((r) => r !== void 0);
|
|
1527
|
+
}
|
|
1528
|
+
async function runCheck(root, command, timeoutMs = 12e4, sandboxed) {
|
|
1529
|
+
const envAllowlist = [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV];
|
|
1530
|
+
const r = sandboxed === void 0 ? await new HostExecutor().run(command, {
|
|
1531
|
+
root,
|
|
1532
|
+
sandbox: false,
|
|
1533
|
+
writePaths: [root],
|
|
1534
|
+
denyWritePaths: [],
|
|
1535
|
+
denyReadPaths: [],
|
|
1536
|
+
network: false,
|
|
1537
|
+
envAllowlist,
|
|
1538
|
+
timeoutMs,
|
|
1539
|
+
maxOutputBytes: 4e3
|
|
1540
|
+
}) : await sandboxed.executor.run(command, {
|
|
1541
|
+
...sandboxed.policy,
|
|
1542
|
+
envAllowlist: [.../* @__PURE__ */ new Set([...sandboxed.policy.envAllowlist, ...TOOLCHAIN_ENV])],
|
|
1543
|
+
timeoutMs,
|
|
1544
|
+
maxOutputBytes: 4e3
|
|
1545
|
+
});
|
|
1546
|
+
const text = `${r.stdout.text}${r.stderr.text}`.trim();
|
|
1547
|
+
const output = text !== "" ? text : `(no output; ${r.timedOut ? "timed out" : `exit code ${r.exitCode}`})`;
|
|
1548
|
+
return { ok: r.exitCode === 0 && !r.timedOut, output };
|
|
1549
|
+
}
|
|
1550
|
+
async function snapshot(root, paths) {
|
|
1551
|
+
const out = /* @__PURE__ */ new Map();
|
|
1552
|
+
for (const path of paths) out.set(path, await readFile4(join7(root, path), "utf8").catch(() => ""));
|
|
1553
|
+
return out;
|
|
1554
|
+
}
|
|
1555
|
+
async function changedFiles(root, before) {
|
|
1556
|
+
const after = await snapshot(root, [...before.keys()]);
|
|
1557
|
+
return [...before.keys()].filter((path) => before.get(path) !== after.get(path));
|
|
1558
|
+
}
|
|
1559
|
+
function isErrorRun(result) {
|
|
1560
|
+
return result.stopReason === "error";
|
|
1561
|
+
}
|
|
1562
|
+
function summarize(results) {
|
|
1563
|
+
const attempted = results.length;
|
|
1564
|
+
const errors = results.filter(isErrorRun).length;
|
|
1565
|
+
const scored = attempted - errors;
|
|
1566
|
+
const passed = results.filter((r) => r.passed).length;
|
|
1567
|
+
return {
|
|
1568
|
+
attempted,
|
|
1569
|
+
errors,
|
|
1570
|
+
scored,
|
|
1571
|
+
passed,
|
|
1572
|
+
...scored === 0 ? {} : { scoredRate: passed / scored },
|
|
1573
|
+
...attempted === 0 ? {} : { attemptedRate: passed / attempted }
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
var percent = (rate) => rate === void 0 ? "\u2013" : `${Math.round(rate * 100)}%`;
|
|
1577
|
+
function formatReport(results, options = {}) {
|
|
1578
|
+
const width = idWidth(results);
|
|
1579
|
+
const rows = results.map(
|
|
1580
|
+
(r) => [
|
|
1581
|
+
r.passed ? "PASS" : isErrorRun(r) ? "ERR " : "FAIL",
|
|
1582
|
+
r.id.padEnd(width),
|
|
1583
|
+
`${String(r.steps).padStart(3)} steps`,
|
|
1584
|
+
(r.unreportedTokens ?? 0) > 0 ? `tok ? (${r.unreportedTokens} reserved)` : `${(r.tokens / 1e3).toFixed(1).padStart(7)}k tok`,
|
|
1585
|
+
r.costUsd === void 0 ? " cost ?" : `${r.costIncomplete === true ? "\u2265" : ""}$${r.costUsd.toFixed(4).padStart(7)}`,
|
|
1586
|
+
`${(r.durationMs / 1e3).toFixed(0).padStart(4)} s`,
|
|
1587
|
+
r.stopReason
|
|
1588
|
+
].join(" ")
|
|
1589
|
+
);
|
|
1590
|
+
const summary = summarize(results);
|
|
1591
|
+
const known = `$${results.reduce((s, r) => s + (r.costUsd ?? 0), 0).toFixed(4)}`;
|
|
1592
|
+
const cost = !results.every((r) => r.costUsd !== void 0) ? "unknown" : results.some((r) => r.costIncomplete === true) ? `\u2265 ${known} (incomplete)` : known;
|
|
1593
|
+
const steps = results.reduce((s, r) => s + r.steps, 0);
|
|
1594
|
+
const counted = `${summary.passed}/${summary.scored} scored runs passed (${percent(summary.scoredRate)}) \xB7 ${summary.passed}/${summary.attempted} of all attempted runs (${percent(summary.attemptedRate)})` + (summary.errors > 0 ? ` \xB7 ${summary.errors} error run(s)` : "");
|
|
1595
|
+
const tokens = results.reduce((s, r) => s + r.tokens, 0);
|
|
1596
|
+
const cached = results.reduce((s, r) => s + (r.cacheReadTokens ?? 0), 0);
|
|
1597
|
+
const share = results.some((r) => (r.unreportedTokens ?? 0) > 0) ? " \xB7 cache share unknown" : tokens === 0 ? "" : ` \xB7 ${Math.round(cached / tokens * 100)}% of tokens from the cache`;
|
|
1598
|
+
const lines2 = [
|
|
1599
|
+
...rows,
|
|
1600
|
+
"",
|
|
1601
|
+
`${counted} \xB7 ${steps} steps \xB7 cost ${cost}${share}`,
|
|
1602
|
+
...evidenceLines(results, options.seed ?? DEFAULT_SEED)
|
|
1603
|
+
];
|
|
1604
|
+
const ids = [...new Set(results.map((r) => r.id))];
|
|
1605
|
+
if (ids.length < results.length) lines2.push("", "Mean per task:", ...meanRows(results, ids));
|
|
1606
|
+
return lines2.join("\n");
|
|
1607
|
+
}
|
|
1608
|
+
function evidenceLines(results, seed) {
|
|
1609
|
+
const out = [];
|
|
1610
|
+
if (results.length > 0 && results.every(isErrorRun)) {
|
|
1611
|
+
return [
|
|
1612
|
+
"No scored run: every run failed with an error (the reasons are above), so this run gives no evidence about the agent."
|
|
1613
|
+
];
|
|
1614
|
+
}
|
|
1615
|
+
const interval = passInterval(results, { seed });
|
|
1616
|
+
if (interval !== void 0 && interval.tasks >= 2) {
|
|
1617
|
+
out.push(
|
|
1618
|
+
`Pass rate over all attempts: ${percent(interval.rate)} (95% interval ${percent(interval.low)}\u2013${percent(interval.high)}; cluster bootstrap over ${interval.tasks} tasks, ${interval.draws} draws, seed ${interval.seed})`
|
|
1619
|
+
);
|
|
1620
|
+
if (interval.low === interval.high) {
|
|
1621
|
+
out.push(
|
|
1622
|
+
" Every draw gave the same rate, so the interval shows no uncertainty: more tasks are needed to measure it."
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (interval.tasks < 10) out.push(` ${roughNote(interval.tasks)}`);
|
|
1626
|
+
}
|
|
1627
|
+
const c = consistency(results);
|
|
1628
|
+
if (c.repeated > 0) {
|
|
1629
|
+
const none = c.nonePassed.length === 0 ? "" : ` (${c.nonePassed.join(", ")})`;
|
|
1630
|
+
const errors = c.onlyErrors.length === 0 ? "" : `; ${c.onlyErrors.length} had only error runs (${c.onlyErrors.join(", ")})`;
|
|
1631
|
+
out.push(
|
|
1632
|
+
`Consistency: ${c.allPassed.length} of ${c.repeated} repeated tasks passed every run; ${c.nonePassed.length} passed none${none}${errors}`
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
const s = spread(results);
|
|
1636
|
+
if (s !== void 0) {
|
|
1637
|
+
const k = (n) => `${(n / 1e3).toFixed(1)}k`;
|
|
1638
|
+
const sec = (ms) => `${Math.round(ms / 1e3)} s`;
|
|
1639
|
+
out.push(
|
|
1640
|
+
`Spread (${s.runs} scored runs): steps median ${s.steps.median.toFixed(1)}, p95 ${s.steps.p95.toFixed(1)} \xB7 tokens median ${k(s.tokens.median)}, p95 ${k(s.tokens.p95)} \xB7 time median ${sec(s.durationMs.median)}, p95 ${sec(s.durationMs.p95)}`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const cost = costPerPass(results);
|
|
1644
|
+
out.push(
|
|
1645
|
+
cost.kind === "unknown" ? "Cost per accepted change: unknown (a run's cost is unknown)" : cost.kind === "none" ? "Cost per accepted change: no passes" : `Cost per accepted change: ${cost.minimum ? "\u2265 " : ""}$${cost.usd.toFixed(4)} (all attempts ${cost.minimum ? "\u2265 " : ""}$${cost.totalUsd.toFixed(4)} / ${cost.passes} pass${cost.passes === 1 ? "" : "es"})`
|
|
1646
|
+
);
|
|
1647
|
+
const u = unexpectedChanges(results);
|
|
1648
|
+
if (u.measured > 0) {
|
|
1649
|
+
out.push(
|
|
1650
|
+
`Unexpected changes: ${u.runs} of ${u.measured} runs, ${u.files} file${u.files === 1 ? "" : "s"}`
|
|
1651
|
+
);
|
|
1652
|
+
const shown = results.filter((r) => (r.unexpected?.length ?? 0) > 0).slice(0, 5);
|
|
1653
|
+
for (const r of shown) {
|
|
1654
|
+
out.push(
|
|
1655
|
+
` ${r.id}${r.attempt === void 0 ? "" : ` (run ${r.attempt})`}: ${(r.unexpected ?? []).join(", ")}`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
if (u.runs > shown.length) out.push(` \u2026 ${u.runs - shown.length} more in report.json`);
|
|
1659
|
+
}
|
|
1660
|
+
const mcp = mcpUse(results);
|
|
1661
|
+
if (mcp !== void 0) out.push(`MCP tool calls: ${mcpUseText(mcp)}`);
|
|
1662
|
+
const failures = failureLine(results);
|
|
1663
|
+
if (failures !== void 0) out.push(failures);
|
|
1664
|
+
return out;
|
|
1665
|
+
}
|
|
1666
|
+
function idWidth(results) {
|
|
1667
|
+
return Math.max(18, ...results.map((r) => r.id.length));
|
|
1668
|
+
}
|
|
1669
|
+
function meanRows(results, ids) {
|
|
1670
|
+
const width = idWidth(results);
|
|
1671
|
+
return ids.map((id) => {
|
|
1672
|
+
const all = results.filter((r) => r.id === id);
|
|
1673
|
+
const runs = all.filter((r) => !isErrorRun(r));
|
|
1674
|
+
const errors = all.length - runs.length;
|
|
1675
|
+
const passesAll = all.filter((r) => r.passed).length;
|
|
1676
|
+
const note = errors > 0 ? ` (${passesAll}/${all.length} of all attempts; ${errors} error run(s))` : "";
|
|
1677
|
+
if (runs.length === 0) return `${"0/0".padEnd(4)} ${id.padEnd(width)}${note}`;
|
|
1678
|
+
const mean = (f) => runs.reduce((s, r) => s + f(r), 0) / runs.length;
|
|
1679
|
+
const passes = runs.filter((r) => r.passed).length;
|
|
1680
|
+
return [
|
|
1681
|
+
`${passes}/${runs.length}`.padEnd(4),
|
|
1682
|
+
id.padEnd(width),
|
|
1683
|
+
`${mean((r) => r.steps).toFixed(1).padStart(5)} steps`,
|
|
1684
|
+
runs.some((r) => (r.unreportedTokens ?? 0) > 0) ? " tok ?" : `${(mean((r) => r.tokens) / 1e3).toFixed(1).padStart(7)}k tok`,
|
|
1685
|
+
runs.every((r) => r.costUsd !== void 0) ? `${runs.some((r) => r.costIncomplete === true) ? "\u2265" : ""}$${mean((r) => r.costUsd ?? 0).toFixed(4).padStart(7)}` : " cost ?",
|
|
1686
|
+
...stepsSd(runs) === void 0 ? [] : [`sd ${stepsSd(runs).toFixed(1)} steps`]
|
|
1687
|
+
].join(" ") + note;
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
var EVAL_BIOME_CONFIG = {
|
|
1691
|
+
formatter: { indentStyle: "space", indentWidth: 2, lineWidth: 100 },
|
|
1692
|
+
linter: { enabled: false },
|
|
1693
|
+
assist: { enabled: false }
|
|
1694
|
+
};
|
|
1695
|
+
async function formatProject(root, biome) {
|
|
1696
|
+
await writeFile4(join7(root, "biome.json"), `${JSON.stringify(EVAL_BIOME_CONFIG, null, 2)}
|
|
1697
|
+
`);
|
|
1698
|
+
const result = await new HostExecutor().run(`${shellWord2(biome)} format --write .`, {
|
|
1699
|
+
root,
|
|
1700
|
+
sandbox: false,
|
|
1701
|
+
writePaths: [root],
|
|
1702
|
+
denyWritePaths: [],
|
|
1703
|
+
denyReadPaths: [],
|
|
1704
|
+
network: false,
|
|
1705
|
+
envAllowlist: [...DEFAULT_ENV_ALLOWLIST],
|
|
1706
|
+
timeoutMs: 6e4,
|
|
1707
|
+
maxOutputBytes: 2e4
|
|
1708
|
+
});
|
|
1709
|
+
if (result.exitCode !== 0) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`Biome could not format the eval project: ${result.stderr.text.trim().split("\n")[0] ?? ""}`
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function shellWord2(text) {
|
|
1716
|
+
return /^[A-Za-z0-9_./:@=+-]+$/.test(text) ? text : `'${text.replace(/'/g, "'\\''")}'`;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
export {
|
|
1720
|
+
DEFAULT_SEED,
|
|
1721
|
+
shuffled,
|
|
1722
|
+
byTask,
|
|
1723
|
+
passInterval,
|
|
1724
|
+
consistency,
|
|
1725
|
+
spread,
|
|
1726
|
+
stepsSd,
|
|
1727
|
+
costPerPass,
|
|
1728
|
+
unexpectedChanges,
|
|
1729
|
+
loadReport,
|
|
1730
|
+
compareReports,
|
|
1731
|
+
formatComparison,
|
|
1732
|
+
writeFiles,
|
|
1733
|
+
MAVEN_POM,
|
|
1734
|
+
MAVEN_CHECK,
|
|
1735
|
+
PYPROJECT,
|
|
1736
|
+
PYTEST_CHECK,
|
|
1737
|
+
lines,
|
|
1738
|
+
TOOLCHAINS,
|
|
1739
|
+
checkToolchains,
|
|
1740
|
+
prepareToolchain,
|
|
1741
|
+
toolchainId,
|
|
1742
|
+
DEFAULT_COMMITS,
|
|
1743
|
+
buildRepoSuite,
|
|
1744
|
+
saveRepoSuite,
|
|
1745
|
+
loadRepoSuite,
|
|
1746
|
+
repoEvalTasks,
|
|
1747
|
+
EVAL_DENY_RULES,
|
|
1748
|
+
REPO_CHECK_TIMEOUT_MS,
|
|
1749
|
+
EVAL_TASK_TIMEOUT_MS,
|
|
1750
|
+
PROVIDER_STALL_MS,
|
|
1751
|
+
PROVIDER_STALLED,
|
|
1752
|
+
MCP_PREPARE_TIMEOUT_MS,
|
|
1753
|
+
ModelWatch,
|
|
1754
|
+
repoTaskSandbox,
|
|
1755
|
+
checkAsEval,
|
|
1756
|
+
runEvalTask,
|
|
1757
|
+
isAuthFailure,
|
|
1758
|
+
isConnectionFailure,
|
|
1759
|
+
saveUnique,
|
|
1760
|
+
PROTECTED_FILES_CHANGED,
|
|
1761
|
+
MCP_PREPARE_FAILED,
|
|
1762
|
+
MCP_NOT_STARTED,
|
|
1763
|
+
isMcpFailure,
|
|
1764
|
+
isProviderStall,
|
|
1765
|
+
isSetupFailure,
|
|
1766
|
+
FAILURE_KINDS,
|
|
1767
|
+
failureKind,
|
|
1768
|
+
failureSummary,
|
|
1769
|
+
failureLine,
|
|
1770
|
+
withStalledRun,
|
|
1771
|
+
runEvals,
|
|
1772
|
+
runCheck,
|
|
1773
|
+
isErrorRun,
|
|
1774
|
+
summarize,
|
|
1775
|
+
formatReport,
|
|
1776
|
+
idWidth,
|
|
1777
|
+
EVAL_BIOME_CONFIG,
|
|
1778
|
+
formatProject
|
|
1779
|
+
};
|
|
1780
|
+
//# sourceMappingURL=chunk-A5DLAXB4.js.map
|