@garuda-agent/garuda 0.0.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +1097 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/acpCommand-FZ2P5JFO.js +153 -0
  7. package/dist/anthropic-NQDBCHJU.js +327 -0
  8. package/dist/chunk-23FXBNBB.js +46 -0
  9. package/dist/chunk-2S62ZNTC.js +320 -0
  10. package/dist/chunk-2TAXSFWN.js +49 -0
  11. package/dist/chunk-36GQIVGT.js +526 -0
  12. package/dist/chunk-3CEPHS4C.js +231 -0
  13. package/dist/chunk-3NJNVZTK.js +123 -0
  14. package/dist/chunk-42DSA4EA.js +116 -0
  15. package/dist/chunk-4JSUUDFQ.js +118 -0
  16. package/dist/chunk-4P26OKTP.js +100 -0
  17. package/dist/chunk-5DEWP3YX.js +139 -0
  18. package/dist/chunk-A2QAIRQL.js +45 -0
  19. package/dist/chunk-ATW7BNIG.js +31 -0
  20. package/dist/chunk-B25VL4RU.js +242 -0
  21. package/dist/chunk-EPKWDCV4.js +9 -0
  22. package/dist/chunk-EU7PA45O.js +324 -0
  23. package/dist/chunk-FARS4R67.js +206 -0
  24. package/dist/chunk-G2AX7MGS.js +36 -0
  25. package/dist/chunk-HRGX53VC.js +173 -0
  26. package/dist/chunk-HUIK3WXO.js +234 -0
  27. package/dist/chunk-INRZR772.js +73 -0
  28. package/dist/chunk-K3SIUOAG.js +138 -0
  29. package/dist/chunk-K7KK7U44.js +9358 -0
  30. package/dist/chunk-KKB5W47S.js +231 -0
  31. package/dist/chunk-N4I6YS3E.js +73 -0
  32. package/dist/chunk-NXJ4XLB5.js +19 -0
  33. package/dist/chunk-OUCV3FCT.js +205 -0
  34. package/dist/chunk-PQNT34LY.js +1247 -0
  35. package/dist/chunk-PZS3V6P3.js +46 -0
  36. package/dist/chunk-S77XDSD4.js +157 -0
  37. package/dist/chunk-SI2QJYCR.js +1341 -0
  38. package/dist/chunk-WMOLXTBU.js +61 -0
  39. package/dist/chunk-YFQJXRIJ.js +193 -0
  40. package/dist/chunk-YQTGMCU6.js +111 -0
  41. package/dist/chunk-ZAE25JM2.js +158 -0
  42. package/dist/cli/index.js +1203 -0
  43. package/dist/create-N4I7TIZE.js +223 -0
  44. package/dist/deadline-F5RJJ2QW.js +86 -0
  45. package/dist/detect-LARDSLLZ.js +10 -0
  46. package/dist/evalCommand-3SSFMZQD.js +2798 -0
  47. package/dist/export-HN3CT7AV.js +158 -0
  48. package/dist/inkChat-34BTFG2K.js +1193 -0
  49. package/dist/install-VRPBZY5H.js +17 -0
  50. package/dist/job-3NYDMP2M.js +31 -0
  51. package/dist/jobCommand-XJ5GULS5.js +30 -0
  52. package/dist/launchd-QVHQ3VNN.js +40 -0
  53. package/dist/lspCommand-XNTBHXLA.js +99 -0
  54. package/dist/manager-4PMSXT47.js +14 -0
  55. package/dist/manager-CSI76AWF.js +847 -0
  56. package/dist/nightCommand-CR6ZHYGP.js +247 -0
  57. package/dist/oauth-W6OK6XXO.js +23 -0
  58. package/dist/openaiCompatible-CJWVJP5M.js +501 -0
  59. package/dist/policy-EJPLEEXP.js +31 -0
  60. package/dist/proxy-XJAHCZB2.js +192 -0
  61. package/dist/remove-LE7Q7K46.js +59 -0
  62. package/dist/run-ML6GSPN4.js +888 -0
  63. package/dist/server-R3GNIC33.js +676 -0
  64. package/dist/servers-6YDGWOZ6.js +24 -0
  65. package/dist/text-J2GD647J.js +11 -0
  66. package/package.json +84 -3
@@ -0,0 +1,2798 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AutoApprover
4
+ } from "./chunk-NXJ4XLB5.js";
5
+ import {
6
+ loadModelsConfig,
7
+ resolveModel
8
+ } from "./chunk-YFQJXRIJ.js";
9
+ import {
10
+ CODE_INDEX_MODES,
11
+ DEFAULT_CODE_INDEX_MODE,
12
+ FileSessionStore,
13
+ Runtime,
14
+ detectProfiles,
15
+ newSessionId
16
+ } from "./chunk-K7KK7U44.js";
17
+ import "./chunk-A2QAIRQL.js";
18
+ import {
19
+ totalTokens
20
+ } from "./chunk-HRGX53VC.js";
21
+ import "./chunk-G2AX7MGS.js";
22
+ import "./chunk-B25VL4RU.js";
23
+ import "./chunk-23FXBNBB.js";
24
+ import "./chunk-EPKWDCV4.js";
25
+ import "./chunk-5DEWP3YX.js";
26
+ import "./chunk-ZAE25JM2.js";
27
+ import "./chunk-ATW7BNIG.js";
28
+ import "./chunk-PZS3V6P3.js";
29
+ import {
30
+ LINK_CANDIDATES,
31
+ cleanArgs
32
+ } from "./chunk-K3SIUOAG.js";
33
+ import {
34
+ MANIFEST,
35
+ TEST_PATH,
36
+ detectTestCommand,
37
+ tail
38
+ } from "./chunk-OUCV3FCT.js";
39
+ import {
40
+ git,
41
+ shellWord
42
+ } from "./chunk-2TAXSFWN.js";
43
+ import {
44
+ DEFAULT_ENV_ALLOWLIST,
45
+ EXECUTOR_NAMES,
46
+ HostExecutor,
47
+ createExecutor,
48
+ expandAllowlist,
49
+ parseSettings
50
+ } from "./chunk-SI2QJYCR.js";
51
+ import "./chunk-WMOLXTBU.js";
52
+ import "./chunk-HUIK3WXO.js";
53
+ import "./chunk-3NJNVZTK.js";
54
+
55
+ // src/cli/evalCommand.ts
56
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, realpathSync as realpathSync3, writeFileSync } from "fs";
57
+ import { createRequire } from "module";
58
+ import { dirname as dirname3, join as join5 } from "path";
59
+
60
+ // src/evals/repoTasks.ts
61
+ import { mkdtempSync, realpathSync } from "fs";
62
+ import { mkdir, readFile, rm as rm2, symlink, writeFile as writeFile2 } from "fs/promises";
63
+ import { tmpdir as tmpdir2 } from "os";
64
+ import { dirname as dirname2, join as join3 } from "path";
65
+ import { z } from "zod";
66
+
67
+ // src/evals/toolchains.ts
68
+ import { mkdtemp, rm } from "fs/promises";
69
+ import { tmpdir } from "os";
70
+ import { join as join2 } from "path";
71
+
72
+ // src/evals/files.ts
73
+ import { mkdirSync } from "fs";
74
+ import { writeFile } from "fs/promises";
75
+ import { dirname, join } from "path";
76
+ async function writeFiles(root, files) {
77
+ for (const [path, content] of Object.entries(files)) {
78
+ const full = join(root, path);
79
+ mkdirSync(dirname(full), { recursive: true });
80
+ await writeFile(full, content);
81
+ }
82
+ }
83
+
84
+ // src/evals/projects.ts
85
+ var MAVEN_POM = `<?xml version="1.0" encoding="UTF-8"?>
86
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
87
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
88
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
89
+ <modelVersion>4.0.0</modelVersion>
90
+ <groupId>dev.garuda.eval</groupId>
91
+ <artifactId>eval</artifactId>
92
+ <version>1.0.0</version>
93
+
94
+ <properties>
95
+ <maven.compiler.release>17</maven.compiler.release>
96
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
97
+ </properties>
98
+
99
+ <dependencies>
100
+ <dependency>
101
+ <groupId>org.junit.jupiter</groupId>
102
+ <artifactId>junit-jupiter</artifactId>
103
+ <version>5.11.4</version>
104
+ <scope>test</scope>
105
+ </dependency>
106
+ </dependencies>
107
+
108
+ <build>
109
+ <plugins>
110
+ <plugin>
111
+ <groupId>org.apache.maven.plugins</groupId>
112
+ <artifactId>maven-resources-plugin</artifactId>
113
+ <version>3.3.1</version>
114
+ </plugin>
115
+ <plugin>
116
+ <groupId>org.apache.maven.plugins</groupId>
117
+ <artifactId>maven-compiler-plugin</artifactId>
118
+ <version>3.13.0</version>
119
+ </plugin>
120
+ <plugin>
121
+ <groupId>org.apache.maven.plugins</groupId>
122
+ <artifactId>maven-surefire-plugin</artifactId>
123
+ <version>3.5.2</version>
124
+ </plugin>
125
+ </plugins>
126
+ </build>
127
+ </project>
128
+ `;
129
+ var MAVEN_CHECK = "test ! -e .mvn && mvn -B -q -o test";
130
+ var PYPROJECT = `[project]
131
+ name = "evalpkg"
132
+ version = "0.1.0"
133
+ requires-python = ">=3.9"
134
+
135
+ [tool.pytest.ini_options]
136
+ pythonpath = ["src"]
137
+ testpaths = ["tests"]
138
+ `;
139
+ var PYTEST_CHECK = "python3 -m pytest -q -c pyproject.toml --noconftest -p no:cacheprovider";
140
+ function isProtectedPath(path) {
141
+ return /^(test|tests|src\/test)\//.test(path) || path === "pom.xml" || path === "pyproject.toml";
142
+ }
143
+ var lines = (...l) => `${l.join("\n")}
144
+ `;
145
+
146
+ // src/evals/toolchains.ts
147
+ var TOOLCHAIN_ENV = ["JAVA_HOME", "GRADLE_USER_HOME", "VIRTUAL_ENV"];
148
+ var TOOLCHAINS = {
149
+ maven: {
150
+ id: "maven",
151
+ title: "Maven with JUnit 5 in ~/.m2",
152
+ files: {
153
+ "pom.xml": MAVEN_POM,
154
+ "src/main/java/probe/Probe.java": "package probe;\n\npublic final class Probe {\n public static int one() {\n return 1;\n }\n}\n",
155
+ "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"
156
+ },
157
+ probe: "mvn -B -q -o test",
158
+ prepare: "mvn -B -q test",
159
+ 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)."
160
+ },
161
+ pytest: {
162
+ id: "pytest",
163
+ title: "Python 3 with pytest",
164
+ files: {
165
+ "pyproject.toml": PYPROJECT,
166
+ "src/probe/__init__.py": "def one():\n return 1\n",
167
+ "tests/test_probe.py": "from probe import one\n\n\ndef test_one():\n assert one() == 1\n"
168
+ },
169
+ probe: "python3 -m pytest -q -p no:cacheprovider",
170
+ hint: "The Python tasks need python3 with pytest 7 or later. Install it, for example with `python3 -m pip install --user pytest`."
171
+ }
172
+ };
173
+ var PROBE_TIMEOUT_MS = 12e4;
174
+ var PREPARE_TIMEOUT_MS = 10 * 6e4;
175
+ async function checkToolchains(ids) {
176
+ return Promise.all(
177
+ [...new Set(ids)].map(async (id) => {
178
+ const toolchain = TOOLCHAINS[id];
179
+ const r = await inScratch(toolchain, toolchain.probe, PROBE_TIMEOUT_MS);
180
+ return { id, ...r };
181
+ })
182
+ );
183
+ }
184
+ async function prepareToolchain(id) {
185
+ const toolchain = TOOLCHAINS[id];
186
+ if (toolchain.prepare === void 0) return { ok: false, output: toolchain.hint };
187
+ return inScratch(toolchain, toolchain.prepare, PREPARE_TIMEOUT_MS);
188
+ }
189
+ function toolchainId(name) {
190
+ const aliases = {
191
+ java: "maven",
192
+ maven: "maven",
193
+ python: "pytest",
194
+ pytest: "pytest"
195
+ };
196
+ return aliases[name.toLowerCase()];
197
+ }
198
+ async function inScratch(toolchain, command, timeoutMs) {
199
+ const root = await mkdtemp(join2(tmpdir(), `garuda-toolchain-${toolchain.id}-`));
200
+ try {
201
+ await writeFiles(root, toolchain.files);
202
+ const r = await new HostExecutor().run(command, {
203
+ root,
204
+ sandbox: false,
205
+ writePaths: [root],
206
+ denyWritePaths: [],
207
+ denyReadPaths: [],
208
+ network: true,
209
+ envAllowlist: [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV],
210
+ timeoutMs,
211
+ maxOutputBytes: 4e3
212
+ });
213
+ const output = `${r.stdout.text}${r.stderr.text}`.trim();
214
+ return { ok: r.exitCode === 0 && !r.timedOut, output };
215
+ } catch (error) {
216
+ return { ok: false, output: error.message };
217
+ } finally {
218
+ await rm(root, { recursive: true, force: true });
219
+ }
220
+ }
221
+
222
+ // src/evals/repoTasks.ts
223
+ var REPO_SUITE_FILE = join3(".garuda", "evals", "repo-suite.json");
224
+ var DEFAULT_COMMITS = 200;
225
+ var DEFAULT_MAX_TASKS = 30;
226
+ var MAX_TASK_FILES = 10;
227
+ var BUILD_TEST_TIMEOUT_MS = 5 * 6e4;
228
+ var testFile = z.object({ path: z.string(), content: z.string().nullable() });
229
+ var taskSchema = z.object({
230
+ id: z.string(),
231
+ sha: z.string(),
232
+ base: z.string(),
233
+ subject: z.string(),
234
+ prompt: z.string(),
235
+ tests: z.array(testFile),
236
+ /** The check: the test command for this task's test files. Absent: the suite's test command. */
237
+ check: z.string().optional()
238
+ });
239
+ var suiteSchema = z.object({
240
+ version: z.literal(1),
241
+ root: z.string(),
242
+ head: z.string(),
243
+ createdAt: z.string(),
244
+ testCommand: z.string(),
245
+ tasks: z.array(taskSchema)
246
+ });
247
+ var DOC_PATH = /\.(md|mdx|rst|txt|adoc)$|(^|\/)docs?\//i;
248
+ var PACKAGE_JSON = /(^|\/)package\.json$/;
249
+ function skipReason(commit) {
250
+ const { files } = commit;
251
+ if (files.length === 0) return "no files";
252
+ const code = files.filter((f) => !DOC_PATH.test(f.path));
253
+ if (code.length > MAX_TASK_FILES) return `more than ${MAX_TASK_FILES} files`;
254
+ if (files.some((f) => f.added === void 0)) return "a binary file";
255
+ if (files.some((f) => MANIFEST.test(f.path) && !PACKAGE_JSON.test(f.path))) {
256
+ return "dependencies changed";
257
+ }
258
+ const tests = code.filter((f) => TEST_PATH.test(f.path));
259
+ if (tests.length === 0) return "no test change";
260
+ if (tests.length === code.length) return "only tests changed";
261
+ return void 0;
262
+ }
263
+ function dependencyChange(diff) {
264
+ for (const line of diff.split("\n")) {
265
+ if (!/^[+-]/.test(line) || /^(\+\+\+|---)/.test(line)) continue;
266
+ if (/[dD]ependencies"/.test(line)) return true;
267
+ const entry = /^[+-]\s*"([^"]+)":\s*"[~^<>=]*\d/.exec(line);
268
+ if (entry !== null && entry[1] !== "version") return true;
269
+ }
270
+ return false;
271
+ }
272
+ function scopedTestCommand(command, files) {
273
+ const list = files.map(shellWord).join(" ");
274
+ if (command.includes("{files}")) return command.replaceAll("{files}", list);
275
+ if (files.length === 0) return command;
276
+ const c = command.trim();
277
+ if (/^npm (run )?test$/.test(c)) return `${c} -- ${list}`;
278
+ 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)) {
279
+ return `${c} ${list}`;
280
+ }
281
+ return c;
282
+ }
283
+ function repoPrompt(commit, tests) {
284
+ return [
285
+ commit.subject.trim(),
286
+ ...commit.body.trim() === "" ? [] : ["", commit.body.trim()],
287
+ "",
288
+ `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.`
289
+ ].join("\n");
290
+ }
291
+ async function recentCommits(executor, root, options = {}) {
292
+ const log = await git(executor, root, [
293
+ "log",
294
+ "--no-merges",
295
+ "--format=%H%x00%s%x00%b%x1e",
296
+ `-n${options.commits ?? DEFAULT_COMMITS}`,
297
+ ...options.since === void 0 ? [] : [`--since=${options.since}`],
298
+ "HEAD"
299
+ ]);
300
+ const out = [];
301
+ for (const entry of log.stdout.split("")) {
302
+ const [sha = "", subject = "", body = ""] = entry.replace(/^\n/, "").split("\0");
303
+ if (!/^[0-9a-f]{40,64}$/.test(sha)) continue;
304
+ const stat = await git(executor, root, ["show", "--no-renames", "--numstat", "--format=", sha]);
305
+ const files = stat.stdout.split("\n").map((line) => /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line)).filter((m) => m !== null).map(
306
+ (m) => m[1] === "-" ? { path: m[3] } : { path: m[3], added: Number(m[1]), removed: Number(m[2]) }
307
+ );
308
+ out.push({ sha, subject, body, files });
309
+ }
310
+ return out;
311
+ }
312
+ async function taskWorktree(executor, root, commit) {
313
+ const dir = realpathSync(mkdtempSync(join3(tmpdir2(), "garuda-repo-task-")));
314
+ await git(executor, root, ["worktree", "add", "--detach", "--quiet", dir, commit]);
315
+ for (const name of LINK_CANDIDATES) {
316
+ try {
317
+ await symlink(join3(root, name), join3(dir, name), "dir");
318
+ } catch {
319
+ }
320
+ }
321
+ return dir;
322
+ }
323
+ async function removeTaskWorktree(executor, root, dir) {
324
+ await git(executor, root, ["worktree", "remove", "--force", dir], { check: false });
325
+ await rm2(dir, { recursive: true, force: true });
326
+ await git(executor, root, ["worktree", "prune"], { check: false });
327
+ }
328
+ async function applyTests(dir, tests) {
329
+ for (const t of tests) {
330
+ const file = join3(dir, t.path);
331
+ if (t.content === null) await rm2(file, { force: true });
332
+ else {
333
+ await mkdir(dirname2(file), { recursive: true });
334
+ await writeFile2(file, t.content);
335
+ }
336
+ }
337
+ }
338
+ async function runTests(executor, dir, command) {
339
+ const result = await executor.run(command, {
340
+ root: dir,
341
+ sandbox: false,
342
+ writePaths: [dir],
343
+ denyWritePaths: [],
344
+ denyReadPaths: [],
345
+ network: false,
346
+ envAllowlist: [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV],
347
+ timeoutMs: BUILD_TEST_TIMEOUT_MS,
348
+ maxOutputBytes: 2e4
349
+ });
350
+ const output = `${result.stdout.text}${result.stderr.text}`;
351
+ return {
352
+ ok: result.exitCode === 0 && !result.timedOut,
353
+ tail: result.timedOut ? "(stopped: too long)" : tail(output, 15)
354
+ };
355
+ }
356
+ async function buildRepoSuite(executor, root, options) {
357
+ const say = options.onProgress ?? (() => {
358
+ });
359
+ const head = (await git(executor, root, ["rev-parse", "HEAD"])).stdout.trim();
360
+ const commits = await recentCommits(executor, root, {
361
+ ...options.commits === void 0 ? {} : { commits: options.commits },
362
+ ...options.since === void 0 ? {} : { since: options.since }
363
+ });
364
+ const skipped = {};
365
+ const skip = (why) => {
366
+ skipped[why] = (skipped[why] ?? 0) + 1;
367
+ };
368
+ const tasks = [];
369
+ let checked = 0;
370
+ let failure;
371
+ const max = options.maxTasks ?? DEFAULT_MAX_TASKS;
372
+ for (const commit of commits) {
373
+ if (tasks.length >= max) break;
374
+ const why = skipReason(commit);
375
+ if (why !== void 0) {
376
+ skip(why);
377
+ continue;
378
+ }
379
+ const parent = await git(
380
+ executor,
381
+ root,
382
+ ["rev-parse", "--verify", "--quiet", `${commit.sha}^`],
383
+ {
384
+ check: false
385
+ }
386
+ );
387
+ if (parent.exitCode !== 0) {
388
+ skip("the first commit (no parent)");
389
+ continue;
390
+ }
391
+ if (commit.files.some((f) => PACKAGE_JSON.test(f.path))) {
392
+ const diff = await git(
393
+ executor,
394
+ root,
395
+ ["show", "--format=", commit.sha, "--", "package.json"],
396
+ {
397
+ check: false
398
+ }
399
+ );
400
+ if (dependencyChange(diff.stdout)) {
401
+ skip("dependencies changed");
402
+ continue;
403
+ }
404
+ }
405
+ const base2 = parent.stdout.trim();
406
+ const testPaths = commit.files.filter((f) => TEST_PATH.test(f.path) && !DOC_PATH.test(f.path)).map((f) => f.path);
407
+ const tests = [];
408
+ for (const path of testPaths) {
409
+ const shown = await git(executor, root, ["show", `${commit.sha}:${path}`], { check: false });
410
+ tests.push({ path, content: shown.exitCode === 0 ? shown.stdout : null });
411
+ }
412
+ const runnable = tests.filter((t) => t.content !== null).map((t) => t.path);
413
+ if (runnable.length === 0) {
414
+ skip("only deletes tests");
415
+ continue;
416
+ }
417
+ const check = scopedTestCommand(options.testCommand, runnable);
418
+ checked++;
419
+ say(`checking ${commit.sha.slice(0, 7)} ${commit.subject}`);
420
+ const dir = await taskWorktree(executor, root, base2);
421
+ try {
422
+ await applyTests(dir, tests);
423
+ if ((await runTests(executor, dir, check)).ok) {
424
+ skip("the tests already pass at the parent");
425
+ continue;
426
+ }
427
+ await git(executor, dir, ["checkout", "--quiet", "--", "."], { check: false });
428
+ await git(executor, dir, cleanArgs(), { check: false });
429
+ await git(executor, dir, ["checkout", "--quiet", "--detach", commit.sha]);
430
+ const atCommit = await runTests(executor, dir, check);
431
+ if (!atCommit.ok) {
432
+ skip("the tests fail at the commit");
433
+ failure ??= { sha: commit.sha, command: check, tail: atCommit.tail };
434
+ continue;
435
+ }
436
+ } finally {
437
+ await removeTaskWorktree(executor, root, dir);
438
+ }
439
+ tasks.push({
440
+ id: `repo-${commit.sha.slice(0, 7)}`,
441
+ sha: commit.sha,
442
+ base: base2,
443
+ subject: commit.subject,
444
+ prompt: repoPrompt(commit, testPaths),
445
+ tests,
446
+ check
447
+ });
448
+ say(`kept ${commit.sha.slice(0, 7)} (${tasks.length}/${max})`);
449
+ }
450
+ return {
451
+ suite: {
452
+ version: 1,
453
+ root,
454
+ head,
455
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
456
+ testCommand: options.testCommand,
457
+ tasks
458
+ },
459
+ checked,
460
+ skipped,
461
+ ...failure === void 0 ? {} : { failure }
462
+ };
463
+ }
464
+ async function saveRepoSuite(suite) {
465
+ const file = join3(suite.root, REPO_SUITE_FILE);
466
+ await mkdir(dirname2(file), { recursive: true });
467
+ await writeFile2(file, `${JSON.stringify(suite, null, 2)}
468
+ `, { mode: 384 });
469
+ return file;
470
+ }
471
+ async function loadRepoSuite(root) {
472
+ let text;
473
+ try {
474
+ text = await readFile(join3(root, REPO_SUITE_FILE), "utf8");
475
+ } catch {
476
+ return void 0;
477
+ }
478
+ const parsed = suiteSchema.safeParse(JSON.parse(text));
479
+ if (!parsed.success) throw new Error(`${REPO_SUITE_FILE}: ${z.prettifyError(parsed.error)}`);
480
+ return parsed.data;
481
+ }
482
+ function repoEvalTasks(suite) {
483
+ return suite.tasks.map((t) => ({
484
+ id: t.id,
485
+ title: t.subject,
486
+ prompt: t.prompt,
487
+ files: {},
488
+ check: t.check ?? suite.testCommand,
489
+ protect: t.tests.filter((f) => f.content !== null).map((f) => f.path),
490
+ solution: {},
491
+ repo: { root: suite.root, base: t.base, tests: t.tests }
492
+ }));
493
+ }
494
+
495
+ // src/evals/runner.ts
496
+ import { existsSync, mkdirSync as mkdirSync2, realpathSync as realpathSync2 } from "fs";
497
+ import { copyFile, mkdtemp as mkdtemp2, readFile as readFile2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
498
+ import { tmpdir as tmpdir3 } from "os";
499
+ import { join as join4 } from "path";
500
+ var EVAL_DENY_RULES = [
501
+ "bash(rm -rf*)",
502
+ "bash(sudo*)",
503
+ "bash(git push*)",
504
+ "bash(curl*)",
505
+ "bash(wget*)"
506
+ ];
507
+ var REPO_CHECK_TIMEOUT_MS = 5 * 6e4;
508
+ var EVAL_TASK_TIMEOUT_MS = 10 * 6e4;
509
+ async function runEvalTask(task4, options) {
510
+ const started = Date.now();
511
+ const host = new HostExecutor();
512
+ let root;
513
+ if (task4.repo === void 0) {
514
+ root = realpathSync2(await mkdtemp2(join4(tmpdir3(), `garuda-eval-${task4.id}-`)));
515
+ await writeFiles(root, task4.files);
516
+ } else {
517
+ root = await taskWorktree(host, task4.repo.root, task4.repo.base);
518
+ await applyTests(root, task4.repo.tests);
519
+ }
520
+ if (options.format !== void 0) await formatProject(root, options.format.biome);
521
+ const protect = task4.protect ?? Object.keys(task4.files).filter(isProtectedPath);
522
+ const before = await snapshot(root, protect);
523
+ const settings = parseSettings({
524
+ // With an allowlist, a host outside it is denied: nobody answers questions in an eval.
525
+ permissions: {
526
+ deny: options.network === void 0 ? EVAL_DENY_RULES : [...EVAL_DENY_RULES, "network"]
527
+ },
528
+ ...options.network === void 0 ? {} : { network: { allow: options.network } },
529
+ // Evals must not depend on the internet.
530
+ web: { enabled: false },
531
+ ...options.executor === void 0 ? {} : { executor: options.executor },
532
+ ...options.codeIndex === void 0 ? {} : { codeIndex: options.codeIndex },
533
+ ...options.maxSteps === void 0 ? {} : { limits: { maxSteps: options.maxSteps } },
534
+ ...options.subagents === void 0 ? {} : { subagents: { enabled: options.subagents } },
535
+ ...options.todo === void 0 ? {} : { todo: { enabled: options.todo } },
536
+ ...options.format === void 0 ? {} : {
537
+ formatters: {
538
+ enabled: options.format.on,
539
+ commands: {
540
+ biome: {
541
+ extensions: ["js", "jsx", "mjs", "cjs", "ts", "tsx", "json"],
542
+ command: [options.format.biome, "format", "--write", "$FILE"]
543
+ }
544
+ }
545
+ }
546
+ },
547
+ ...options.keepThinking === void 0 ? {} : { thinking: { keepBlocks: options.keepThinking } }
548
+ });
549
+ const store = new FileSessionStore(root);
550
+ const runtime = await Runtime.create({
551
+ root,
552
+ modelId: options.modelId,
553
+ model: async () => options.model(),
554
+ ...options.modelInfo === void 0 ? {} : { modelInfo: options.modelInfo },
555
+ ...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens },
556
+ ...options.subagentModel === void 0 ? {} : { subagentModel: options.subagentModel },
557
+ approver: new AutoApprover("once"),
558
+ store,
559
+ settings,
560
+ // The user's own MCP servers must not change eval results.
561
+ mcp: false,
562
+ hooks: false,
563
+ ...options.policy === void 0 ? {} : { policy: options.policy },
564
+ commands: false,
565
+ // The list comes from the eval command line: the user typed it.
566
+ ...options.network === void 0 ? {} : { network: { approved: true } },
567
+ ...options.lsp === void 0 ? {} : { lsp: { enabled: options.lsp } },
568
+ ...options.onEvent === void 0 ? {} : { onEvent: (event) => options.onEvent?.(task4.id, event) }
569
+ });
570
+ const result = {
571
+ id: task4.id,
572
+ title: task4.title,
573
+ passed: false,
574
+ stopReason: "error",
575
+ steps: 0,
576
+ tokens: 0,
577
+ costUsd: void 0,
578
+ durationMs: 0
579
+ };
580
+ const limitMs = options.taskTimeoutMs ?? EVAL_TASK_TIMEOUT_MS;
581
+ const timeout = AbortSignal.timeout(limitMs);
582
+ try {
583
+ const run = await runtime.runTurn(task4.prompt, timeout);
584
+ result.stopReason = run.stopReason;
585
+ result.steps = run.steps;
586
+ } catch (error) {
587
+ runtime.recordStop("error");
588
+ if (timeout.aborted) {
589
+ result.stopReason = "timeout";
590
+ result.reason = `The task took longer than ${limitMs / 6e4} minutes.`;
591
+ } else {
592
+ result.reason = `The run failed: ${error.message}`;
593
+ }
594
+ } finally {
595
+ await runtime.close();
596
+ runtime.executor.shutdown();
597
+ }
598
+ const session = runtime.session;
599
+ if (session !== void 0) {
600
+ result.tokens = totalTokens(session.usage);
601
+ result.costUsd = session.costUsd;
602
+ result.cacheReadTokens = session.usage.cacheReadTokens;
603
+ if (options.outDir !== void 0) {
604
+ mkdirSync2(options.outDir, { recursive: true });
605
+ let target = join4(options.outDir, `${task4.id}.jsonl`);
606
+ for (let n = 2; existsSync(target); n++)
607
+ target = join4(options.outDir, `${task4.id}-${n}.jsonl`);
608
+ await copyFile(store.path(session.id), target);
609
+ result.sessionFile = target;
610
+ }
611
+ }
612
+ if (result.reason === void 0) {
613
+ const changed = await changedFiles(root, before);
614
+ const checkMs = task4.repo === void 0 ? 12e4 : REPO_CHECK_TIMEOUT_MS;
615
+ const check = await runCheck(
616
+ root,
617
+ task4.check,
618
+ checkMs,
619
+ runtime.executor.isolation === "none" ? void 0 : { executor: runtime.executor, policy: runtime.execPolicy(checkMs) }
620
+ );
621
+ if (changed.length > 0)
622
+ result.reason = `The agent changed protected files: ${changed.join(", ")}.`;
623
+ else if (!check.ok) result.reason = `The check failed:
624
+ ${check.output}`;
625
+ else result.passed = true;
626
+ }
627
+ result.durationMs = Date.now() - started;
628
+ if (!options.keep) {
629
+ if (task4.repo === void 0) await rm3(root, { recursive: true, force: true });
630
+ else await removeTaskWorktree(host, task4.repo.root, root);
631
+ }
632
+ return result;
633
+ }
634
+ async function runEvals(tasks, options, onResult) {
635
+ const results = new Array(tasks.length);
636
+ let next = 0;
637
+ const worker = async () => {
638
+ for (let i = next++; i < tasks.length; i = next++) {
639
+ const result = await runEvalTask(tasks[i], options);
640
+ results[i] = result;
641
+ onResult?.(result);
642
+ }
643
+ };
644
+ const width = Math.max(1, Math.min(options.parallel ?? 1, tasks.length));
645
+ await Promise.all(Array.from({ length: width }, worker));
646
+ return results;
647
+ }
648
+ async function runCheck(root, command, timeoutMs = 12e4, sandboxed) {
649
+ const envAllowlist = [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV];
650
+ const r = sandboxed === void 0 ? await new HostExecutor().run(command, {
651
+ root,
652
+ sandbox: false,
653
+ writePaths: [root],
654
+ denyWritePaths: [],
655
+ denyReadPaths: [],
656
+ network: false,
657
+ envAllowlist,
658
+ timeoutMs,
659
+ maxOutputBytes: 4e3
660
+ }) : await sandboxed.executor.run(command, {
661
+ ...sandboxed.policy,
662
+ envAllowlist: [.../* @__PURE__ */ new Set([...sandboxed.policy.envAllowlist, ...TOOLCHAIN_ENV])],
663
+ timeoutMs,
664
+ maxOutputBytes: 4e3
665
+ });
666
+ const output = `${r.stdout.text}${r.stderr.text}`.trim();
667
+ return { ok: r.exitCode === 0 && !r.timedOut, output };
668
+ }
669
+ async function snapshot(root, paths) {
670
+ const out = /* @__PURE__ */ new Map();
671
+ for (const path of paths) out.set(path, await readFile2(join4(root, path), "utf8").catch(() => ""));
672
+ return out;
673
+ }
674
+ async function changedFiles(root, before) {
675
+ const after = await snapshot(root, [...before.keys()]);
676
+ return [...before.keys()].filter((path) => before.get(path) !== after.get(path));
677
+ }
678
+ function isErrorRun(result) {
679
+ return result.stopReason === "error";
680
+ }
681
+ function formatReport(results) {
682
+ const rows = results.map(
683
+ (r) => [
684
+ r.passed ? "PASS" : isErrorRun(r) ? "ERR " : "FAIL",
685
+ r.id.padEnd(18),
686
+ `${String(r.steps).padStart(3)} steps`,
687
+ `${(r.tokens / 1e3).toFixed(1).padStart(7)}k tok`,
688
+ r.costUsd === void 0 ? " cost ?" : `$${r.costUsd.toFixed(4).padStart(7)}`,
689
+ `${(r.durationMs / 1e3).toFixed(0).padStart(4)} s`,
690
+ r.stopReason
691
+ ].join(" ")
692
+ );
693
+ const passed = results.filter((r) => r.passed).length;
694
+ const errors = results.filter(isErrorRun).length;
695
+ const cost = results.every((r) => r.costUsd !== void 0) ? `$${results.reduce((s, r) => s + (r.costUsd ?? 0), 0).toFixed(4)}` : "unknown";
696
+ const steps = results.reduce((s, r) => s + r.steps, 0);
697
+ const counted = `${passed}/${results.length - errors} passed${errors > 0 ? ` \xB7 ${errors} error run(s) not counted` : ""}`;
698
+ const tokens = results.reduce((s, r) => s + r.tokens, 0);
699
+ const cached = results.reduce((s, r) => s + (r.cacheReadTokens ?? 0), 0);
700
+ const share = tokens === 0 ? "" : ` \xB7 ${Math.round(cached / tokens * 100)}% of tokens from the cache`;
701
+ const lines4 = [...rows, "", `${counted} \xB7 ${steps} steps \xB7 cost ${cost}${share}`];
702
+ const ids = [...new Set(results.map((r) => r.id))];
703
+ if (ids.length < results.length) lines4.push("", "Mean per task:", ...meanRows(results, ids));
704
+ return lines4.join("\n");
705
+ }
706
+ function meanRows(results, ids) {
707
+ return ids.map((id) => {
708
+ const all = results.filter((r) => r.id === id);
709
+ const runs = all.filter((r) => !isErrorRun(r));
710
+ const errors = all.length - runs.length;
711
+ const note = errors > 0 ? ` (${errors} error run(s) left out)` : "";
712
+ if (runs.length === 0) return `${"0/0".padEnd(4)} ${id.padEnd(18)}${note}`;
713
+ const mean = (f) => runs.reduce((s, r) => s + f(r), 0) / runs.length;
714
+ const passes = runs.filter((r) => r.passed).length;
715
+ return [
716
+ `${passes}/${runs.length}`.padEnd(4),
717
+ id.padEnd(18),
718
+ `${mean((r) => r.steps).toFixed(1).padStart(5)} steps`,
719
+ `${(mean((r) => r.tokens) / 1e3).toFixed(1).padStart(7)}k tok`,
720
+ runs.every((r) => r.costUsd !== void 0) ? `$${mean((r) => r.costUsd ?? 0).toFixed(4).padStart(7)}` : " cost ?"
721
+ ].join(" ") + note;
722
+ });
723
+ }
724
+ var EVAL_BIOME_CONFIG = {
725
+ formatter: { indentStyle: "space", indentWidth: 2, lineWidth: 100 },
726
+ linter: { enabled: false },
727
+ assist: { enabled: false }
728
+ };
729
+ async function formatProject(root, biome) {
730
+ await writeFile3(join4(root, "biome.json"), `${JSON.stringify(EVAL_BIOME_CONFIG, null, 2)}
731
+ `);
732
+ const result = await new HostExecutor().run(`${shellWord2(biome)} format --write .`, {
733
+ root,
734
+ sandbox: false,
735
+ writePaths: [root],
736
+ denyWritePaths: [],
737
+ denyReadPaths: [],
738
+ network: false,
739
+ envAllowlist: [...DEFAULT_ENV_ALLOWLIST],
740
+ timeoutMs: 6e4,
741
+ maxOutputBytes: 2e4
742
+ });
743
+ if (result.exitCode !== 0) {
744
+ throw new Error(
745
+ `Biome could not format the eval project: ${result.stderr.text.trim().split("\n")[0] ?? ""}`
746
+ );
747
+ }
748
+ }
749
+ function shellWord2(text) {
750
+ return /^[A-Za-z0-9_./:@=+-]+$/.test(text) ? text : `'${text.replace(/'/g, "'\\''")}'`;
751
+ }
752
+
753
+ // src/evals/shopkit.ts
754
+ var lines2 = (...l) => `${l.join("\n")}
755
+ `;
756
+ var entity = (name, prefix, fields, amount) => ({
757
+ name,
758
+ Name: name[0]?.toUpperCase() + name.slice(1),
759
+ prefix,
760
+ fields,
761
+ ...amount === void 0 ? {} : { amount }
762
+ });
763
+ var ENTITIES = [
764
+ entity("product", "prd", ["name", "price"], "price"),
765
+ entity("customer", "cus", ["name", "email"]),
766
+ entity("cart", "crt", ["customerId"]),
767
+ entity("shipment", "shp", ["orderId", "carrier"]),
768
+ entity("warehouse", "whs", ["name", "city"]),
769
+ entity("supplier", "sup", ["name", "country"]),
770
+ entity("coupon", "cpn", ["code", "percent"]),
771
+ entity("review", "rev", ["productId", "stars"]),
772
+ entity("category", "cat", ["name"]),
773
+ entity("payment", "pay", ["orderId", "amount"], "amount"),
774
+ entity("refund", "ref", ["paymentId", "amount"], "amount"),
775
+ entity("address", "adr", ["customerId", "line1", "postcode"]),
776
+ entity("price", "prc", ["productId", "value"]),
777
+ entity("tax", "tax", ["region", "rate"]),
778
+ entity("discount", "dsc", ["name", "amount"], "amount"),
779
+ entity("report", "rpt", ["title"]),
780
+ entity("user", "usr", ["login", "role"])
781
+ ];
782
+ function model(e) {
783
+ return lines2(
784
+ 'import { newId } from "../../core/ids.js";',
785
+ "",
786
+ `/** Create a ${e.name} record. */`,
787
+ `export function create${e.Name}(fields) {`,
788
+ ` return { id: newId("${e.prefix}"), ...fields };`,
789
+ "}"
790
+ );
791
+ }
792
+ function validate(e) {
793
+ return lines2(
794
+ 'import { ValidationError } from "../../core/errors.js";',
795
+ "",
796
+ `const REQUIRED = [${e.fields.map((f) => `"${f}"`).join(", ")}];`,
797
+ "",
798
+ `export function validate${e.Name}(input) {`,
799
+ " for (const key of REQUIRED) {",
800
+ ` if (input?.[key] === undefined) throw new ValidationError(\`${e.name}.\${key} is required\`);`,
801
+ " }",
802
+ " return input;",
803
+ "}"
804
+ );
805
+ }
806
+ function repository(e) {
807
+ return lines2(
808
+ `export class ${e.Name}Repository {`,
809
+ " #items = new Map();",
810
+ "",
811
+ " add(item) {",
812
+ " this.#items.set(item.id, item);",
813
+ " return item;",
814
+ " }",
815
+ "",
816
+ " get(id) {",
817
+ " return this.#items.get(id);",
818
+ " }",
819
+ "",
820
+ " listAll() {",
821
+ " return [...this.#items.values()];",
822
+ " }",
823
+ "",
824
+ " remove(id) {",
825
+ " return this.#items.delete(id);",
826
+ " }",
827
+ "}"
828
+ );
829
+ }
830
+ function service(e) {
831
+ const money2 = e.amount !== void 0;
832
+ return lines2(
833
+ 'import { events } from "../../core/events.js";',
834
+ ...money2 ? ['import { formatPrice, toCents } from "../../core/money.js";'] : [],
835
+ `import { create${e.Name} } from "./model.js";`,
836
+ `import { ${e.Name}Repository } from "./repository.js";`,
837
+ `import { validate${e.Name} } from "./validate.js";`,
838
+ "",
839
+ `export class ${e.Name}Service {`,
840
+ ` constructor({ repo = new ${e.Name}Repository(), bus = events } = {}) {`,
841
+ " this.repo = repo;",
842
+ " this.bus = bus;",
843
+ " }",
844
+ "",
845
+ " create(input) {",
846
+ ` const item = this.repo.add(create${e.Name}(validate${e.Name}(input)));`,
847
+ ` this.bus.emit("${e.name}.created", item);`,
848
+ " return item;",
849
+ " }",
850
+ "",
851
+ " find(id) {",
852
+ " return this.repo.get(id);",
853
+ " }",
854
+ "",
855
+ " all() {",
856
+ " return this.repo.listAll();",
857
+ " }",
858
+ ...money2 ? [
859
+ "",
860
+ " describe(item) {",
861
+ ` return \`${e.Name} \${item.id}: \${formatPrice(toCents(item.${e.amount}))}\`;`,
862
+ " }"
863
+ ] : [],
864
+ "}"
865
+ );
866
+ }
867
+ function entityTest(e) {
868
+ const sample = Object.fromEntries(e.fields.map((f, i) => [f, i === 0 ? `x-${e.name}` : 3]));
869
+ return lines2(
870
+ 'import assert from "node:assert/strict";',
871
+ 'import { test } from "node:test";',
872
+ 'import { EventBus } from "../../src/core/events.js";',
873
+ `import { ${e.Name}Service } from "../../src/domains/${e.name}/service.js";`,
874
+ "",
875
+ `test("${e.name}: create, find, list", () => {`,
876
+ ` const service = new ${e.Name}Service({ bus: new EventBus() });`,
877
+ ` const item = service.create(${JSON.stringify(sample)});`,
878
+ " assert.equal(service.find(item.id), item);",
879
+ " assert.equal(service.all().length, 1);",
880
+ ` assert.match(item.id, /^${e.prefix}_/);`,
881
+ "});",
882
+ "",
883
+ `test("${e.name}: validation", () => {`,
884
+ ` const service = new ${e.Name}Service({ bus: new EventBus() });`,
885
+ ` assert.throws(() => service.create({}), /${e.name}\\.${e.fields[0]} is required/);`,
886
+ "});"
887
+ );
888
+ }
889
+ var CORE = {
890
+ "src/core/ids.js": lines2(
891
+ "let next = 1;",
892
+ "",
893
+ "/** Ids like prd_000001. Sequential, so tests are stable. */",
894
+ "export function newId(prefix) {",
895
+ ' return `${prefix}_${String(next++).padStart(6, "0")}`;',
896
+ "}"
897
+ ),
898
+ "src/core/errors.js": lines2(
899
+ "export class ValidationError extends Error {",
900
+ ' name = "ValidationError";',
901
+ "}",
902
+ "",
903
+ "export class NotFoundError extends Error {",
904
+ ' name = "NotFoundError";',
905
+ "}"
906
+ ),
907
+ "src/core/events.js": lines2(
908
+ "export class EventBus {",
909
+ " #handlers = new Map();",
910
+ "",
911
+ " on(name, handler) {",
912
+ " const list = this.#handlers.get(name) ?? [];",
913
+ " list.push(handler);",
914
+ " this.#handlers.set(name, list);",
915
+ " }",
916
+ "",
917
+ " emit(name, payload) {",
918
+ " for (const handler of this.#handlers.get(name) ?? []) handler(payload);",
919
+ " }",
920
+ "}",
921
+ "",
922
+ "/** The shared bus. Tests pass their own. */",
923
+ "export const events = new EventBus();"
924
+ ),
925
+ "src/core/money.js": lines2(
926
+ "/** Amounts are numbers in pounds. Totals are integers in pence. */",
927
+ "export function toCents(amount) {",
928
+ " return Math.round(amount * 100);",
929
+ "}",
930
+ "",
931
+ 'export function formatPrice(cents, symbol = "\xA3") {',
932
+ ' const sign = cents < 0 ? "-" : "";',
933
+ " const abs = Math.abs(cents);",
934
+ ' return `${sign}${symbol}${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, "0")}`;',
935
+ "}"
936
+ ),
937
+ "src/core/strings.js": lines2(
938
+ "export function capitalize(text) {",
939
+ " return text.charAt(0).toUpperCase() + text.slice(1);",
940
+ "}",
941
+ "",
942
+ "export function slug(text) {",
943
+ ' return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");',
944
+ "}",
945
+ "",
946
+ "export function truncate(text, max) {",
947
+ " return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;",
948
+ "}",
949
+ "",
950
+ 'export function padLeft(text, width, fill = " ") {',
951
+ " return String(text).padStart(width, fill);",
952
+ "}"
953
+ ),
954
+ "src/core/dates.js": lines2(
955
+ "const DAY = 24 * 60 * 60 * 1000;",
956
+ "",
957
+ "export function addDays(date, days) {",
958
+ " return new Date(date.getTime() + days * DAY);",
959
+ "}",
960
+ "",
961
+ "export function toIsoDate(date) {",
962
+ " return date.toISOString().slice(0, 10);",
963
+ "}",
964
+ "",
965
+ "export function isWeekend(date) {",
966
+ " const day = date.getUTCDay();",
967
+ " return day === 0 || day === 6;",
968
+ "}"
969
+ ),
970
+ "src/core/config.js": lines2(
971
+ 'const DEFAULTS = { port: 3000, currency: "GBP", invoiceDays: 30 };',
972
+ "",
973
+ "export function loadConfig(overrides = {}) {",
974
+ " return { ...DEFAULTS, ...overrides };",
975
+ "}"
976
+ ),
977
+ "src/core/logger.js": lines2(
978
+ "export function createLogger(name, sink = () => {}) {",
979
+ " return {",
980
+ " info: (message) => sink(`[${name}] ${message}`),",
981
+ " error: (message) => sink(`[${name}] ERROR ${message}`),",
982
+ " };",
983
+ "}"
984
+ )
985
+ };
986
+ var HAND_WRITTEN = {
987
+ // order: totals, events, describe
988
+ "src/domains/order/model.js": model(entity("order", "ord", [])),
989
+ "src/domains/order/repository.js": repository(entity("order", "ord", [])),
990
+ "src/domains/order/service.js": lines2(
991
+ 'import { NotFoundError, ValidationError } from "../../core/errors.js";',
992
+ 'import { events } from "../../core/events.js";',
993
+ 'import { formatPrice, toCents } from "../../core/money.js";',
994
+ 'import { createOrder } from "./model.js";',
995
+ 'import { OrderRepository } from "./repository.js";',
996
+ "",
997
+ "export class OrderService {",
998
+ " constructor({ products, repo = new OrderRepository(), bus = events } = {}) {",
999
+ " this.products = products;",
1000
+ " this.repo = repo;",
1001
+ " this.bus = bus;",
1002
+ " }",
1003
+ "",
1004
+ " create({ customerId, lines }) {",
1005
+ ' if (!lines?.length) throw new ValidationError("order.lines is required");',
1006
+ " let totalCents = 0;",
1007
+ " for (const line of lines) {",
1008
+ " const product = this.products.find(line.productId);",
1009
+ " if (!product) throw new NotFoundError(`product ${line.productId}`);",
1010
+ " totalCents += toCents(product.price) * line.qty;",
1011
+ " }",
1012
+ " const order = this.repo.add(createOrder({ customerId, lines, totalCents }));",
1013
+ ' this.bus.emit("order.created", order);',
1014
+ " return order;",
1015
+ " }",
1016
+ "",
1017
+ " find(id) {",
1018
+ " return this.repo.get(id);",
1019
+ " }",
1020
+ "",
1021
+ " all() {",
1022
+ " return this.repo.listAll();",
1023
+ " }",
1024
+ "",
1025
+ " describe(order) {",
1026
+ " return `Order ${order.id}: ${formatPrice(order.totalCents)}`;",
1027
+ " }",
1028
+ "}"
1029
+ ),
1030
+ // stock: reacts to order.created
1031
+ "src/domains/stock/service.js": lines2(
1032
+ 'import { events } from "../../core/events.js";',
1033
+ "",
1034
+ "/** Stock levels per product. Orders reserve stock through the order.created event. */",
1035
+ "export class StockService {",
1036
+ " #levels = new Map();",
1037
+ "",
1038
+ " constructor({ bus = events } = {}) {",
1039
+ ' bus.on("order.created", (order) => this.reserve(order));',
1040
+ " }",
1041
+ "",
1042
+ " set(productId, qty) {",
1043
+ " this.#levels.set(productId, qty);",
1044
+ " }",
1045
+ "",
1046
+ " level(productId) {",
1047
+ " return this.#levels.get(productId) ?? 0;",
1048
+ " }",
1049
+ "",
1050
+ " reserve(order) {",
1051
+ " for (const line of order.lines) this.set(line.productId, this.level(line.productId) - line.qty);",
1052
+ " }",
1053
+ "}"
1054
+ ),
1055
+ // invoice: formats the order total
1056
+ "src/domains/invoice/service.js": lines2(
1057
+ 'import { loadConfig } from "../../core/config.js";',
1058
+ 'import { addDays, toIsoDate } from "../../core/dates.js";',
1059
+ 'import { formatPrice } from "../../core/money.js";',
1060
+ "",
1061
+ "export class InvoiceService {",
1062
+ " constructor({ config = loadConfig() } = {}) {",
1063
+ " this.config = config;",
1064
+ " }",
1065
+ "",
1066
+ " fromOrder(order, issued = new Date(Date.UTC(2026, 0, 1))) {",
1067
+ " return {",
1068
+ " orderId: order.id,",
1069
+ " amount: formatPrice(order.totalCents),",
1070
+ " issuedOn: toIsoDate(issued),",
1071
+ " dueOn: toIsoDate(addDays(issued, this.config.invoiceDays)),",
1072
+ " };",
1073
+ " }",
1074
+ "}"
1075
+ ),
1076
+ "src/api/routes.js": lines2(
1077
+ 'import { loadConfig } from "../core/config.js";',
1078
+ 'import { createLogger } from "../core/logger.js";',
1079
+ 'import { capitalize, slug } from "../core/strings.js";',
1080
+ 'import { CustomerService } from "../domains/customer/service.js";',
1081
+ 'import { OrderService } from "../domains/order/service.js";',
1082
+ 'import { ProductService } from "../domains/product/service.js";',
1083
+ 'import { ShipmentService } from "../domains/shipment/service.js";',
1084
+ "",
1085
+ "/** Build the route table. Handlers take a body and return a result. */",
1086
+ 'export function buildRoutes(log = createLogger("api")) {',
1087
+ " const config = loadConfig();",
1088
+ " const products = new ProductService();",
1089
+ " const orders = new OrderService({ products });",
1090
+ " const customers = new CustomerService();",
1091
+ " const shipments = new ShipmentService();",
1092
+ " const routes = {",
1093
+ ' "POST /products": (body) => products.create(body),',
1094
+ ' "POST /customers": (body) => customers.create(body),',
1095
+ ' "POST /orders": (body) => orders.create(body),',
1096
+ ' "POST /shipments": (body) => shipments.create(body),',
1097
+ " };",
1098
+ " log.info(`${Object.keys(routes).length} routes on port ${config.port}`);",
1099
+ " return routes;",
1100
+ "}",
1101
+ "",
1102
+ "export function routeTitle(route) {",
1103
+ ' return capitalize(slug(route.split(" ")[1] ?? ""));',
1104
+ "}"
1105
+ )
1106
+ };
1107
+ var imp = (names, from) => [
1108
+ 'import assert from "node:assert/strict";',
1109
+ 'import { test } from "node:test";',
1110
+ `import { ${names} } from "${from}";`
1111
+ ].join("\n");
1112
+ var FEATURE_TESTS = {
1113
+ "test/features/order-total.test.js": lines2(
1114
+ imp("EventBus", "../../src/core/events.js"),
1115
+ 'import { OrderService } from "../../src/domains/order/service.js";',
1116
+ 'import { ProductService } from "../../src/domains/product/service.js";',
1117
+ "",
1118
+ 'test("order total in pence", () => {',
1119
+ " const bus = new EventBus();",
1120
+ " const products = new ProductService({ bus });",
1121
+ ' const pen = products.create({ name: "pen", price: 0.29 });',
1122
+ ' const pad = products.create({ name: "pad", price: 1.15 });',
1123
+ " const orders = new OrderService({ products, bus });",
1124
+ ' const order = orders.create({ customerId: "c1", lines: [{ productId: pen.id, qty: 3 }, { productId: pad.id, qty: 1 }] });',
1125
+ " assert.equal(order.totalCents, 202);",
1126
+ " assert.equal(orders.describe(order), `Order ${order.id}: \xA32.02`);",
1127
+ "});"
1128
+ ),
1129
+ "test/features/invoice.test.js": lines2(
1130
+ imp("EventBus", "../../src/core/events.js"),
1131
+ 'import { InvoiceService } from "../../src/domains/invoice/service.js";',
1132
+ 'import { OrderService } from "../../src/domains/order/service.js";',
1133
+ 'import { ProductService } from "../../src/domains/product/service.js";',
1134
+ "",
1135
+ 'test("invoice from an order", () => {',
1136
+ " const bus = new EventBus();",
1137
+ " const products = new ProductService({ bus });",
1138
+ ' const tea = products.create({ name: "tea", price: 0.57 });',
1139
+ ' const order = new OrderService({ products, bus }).create({ customerId: "c1", lines: [{ productId: tea.id, qty: 1 }] });',
1140
+ " const invoice = new InvoiceService().fromOrder(order);",
1141
+ ' assert.deepEqual(invoice, { orderId: order.id, amount: "\xA30.57", issuedOn: "2026-01-01", dueOn: "2026-01-31" });',
1142
+ "});"
1143
+ ),
1144
+ "test/features/stock.test.js": lines2(
1145
+ imp("EventBus", "../../src/core/events.js"),
1146
+ 'import { OrderService } from "../../src/domains/order/service.js";',
1147
+ 'import { ProductService } from "../../src/domains/product/service.js";',
1148
+ 'import { StockService } from "../../src/domains/stock/service.js";',
1149
+ "",
1150
+ 'test("an order reserves stock", () => {',
1151
+ " const bus = new EventBus();",
1152
+ " const products = new ProductService({ bus });",
1153
+ " const stock = new StockService({ bus });",
1154
+ ' const mug = products.create({ name: "mug", price: 4 });',
1155
+ " stock.set(mug.id, 10);",
1156
+ ' new OrderService({ products, bus }).create({ customerId: "c1", lines: [{ productId: mug.id, qty: 3 }] });',
1157
+ " assert.equal(stock.level(mug.id), 7);",
1158
+ "});"
1159
+ ),
1160
+ "test/api/routes.test.js": lines2(
1161
+ imp("buildRoutes, routeTitle", "../../src/api/routes.js"),
1162
+ "",
1163
+ 'test("routes", () => {',
1164
+ " const logs = [];",
1165
+ " const routes = buildRoutes({ info: (m) => logs.push(m), error: () => {} });",
1166
+ ' assert.deepEqual(Object.keys(routes).sort(), ["POST /customers", "POST /orders", "POST /products", "POST /shipments"]);',
1167
+ ' assert.deepEqual(logs, ["4 routes on port 3000"]);',
1168
+ ' assert.equal(routeTitle("POST /products"), "Products");',
1169
+ "});"
1170
+ ),
1171
+ "test/core/money.test.js": lines2(
1172
+ imp("formatPrice, toCents", "../../src/core/money.js"),
1173
+ "",
1174
+ 'test("money", () => {',
1175
+ " assert.equal(toCents(0.29), 29);",
1176
+ ' assert.equal(formatPrice(1205), "\xA312.05");',
1177
+ ' assert.equal(formatPrice(-5), "-\xA30.05");',
1178
+ "});"
1179
+ ),
1180
+ "test/core/strings.test.js": lines2(
1181
+ imp("capitalize, padLeft, slug, truncate", "../../src/core/strings.js"),
1182
+ "",
1183
+ 'test("strings", () => {',
1184
+ ' assert.equal(capitalize("abc"), "Abc");',
1185
+ ' assert.equal(slug("Hello World!"), "hello-world");',
1186
+ ' assert.equal(truncate("abcdef", 4), "abc\u2026");',
1187
+ ' assert.equal(padLeft(7, 3, "0"), "007");',
1188
+ "});"
1189
+ )
1190
+ };
1191
+ function shopkit() {
1192
+ const files = {
1193
+ "package.json": '{ "name": "shopkit", "version": "2.4.0", "type": "module", "private": true }\n',
1194
+ "README.md": lines2(
1195
+ "# shopkit",
1196
+ "",
1197
+ "A small shop backend. Domains live in src/domains/<name>/ (model, repository, service, validate).",
1198
+ "Shared code lives in src/core/. The route table is in src/api/routes.js.",
1199
+ "Run the tests with: node --test"
1200
+ ),
1201
+ ...CORE,
1202
+ ...HAND_WRITTEN,
1203
+ ...FEATURE_TESTS
1204
+ };
1205
+ for (const e of ENTITIES) {
1206
+ files[`src/domains/${e.name}/model.js`] = model(e);
1207
+ files[`src/domains/${e.name}/validate.js`] = validate(e);
1208
+ files[`src/domains/${e.name}/repository.js`] = repository(e);
1209
+ files[`src/domains/${e.name}/service.js`] = service(e);
1210
+ files[`test/domains/${e.name}.test.js`] = entityTest(e);
1211
+ }
1212
+ return files;
1213
+ }
1214
+
1215
+ // src/evals/hardTasks.ts
1216
+ var BASE = shopkit();
1217
+ var base = (path) => {
1218
+ const content = BASE[path];
1219
+ if (content === void 0) throw new Error(`shopkit has no ${path}`);
1220
+ return content;
1221
+ };
1222
+ function task(fields) {
1223
+ const { change, ...rest } = fields;
1224
+ return { ...rest, files: { ...BASE, ...change } };
1225
+ }
1226
+ var replaceIn = (path, from, to) => {
1227
+ const content = base(path);
1228
+ if (!content.includes(from)) throw new Error(`${path} has no "${from}"`);
1229
+ return content.split(from).join(to);
1230
+ };
1231
+ var renamed = Object.fromEntries(
1232
+ Object.entries(BASE).filter(([path, content]) => path.startsWith("src/") && content.includes("formatPrice")).map(([path, content]) => [path, content.split("formatPrice").join("formatMoney")])
1233
+ );
1234
+ var HARD_TASKS = [
1235
+ task({
1236
+ id: "hard-rounding",
1237
+ title: "Find a root cause in shared code",
1238
+ prompt: "Several tests about orders and invoices fail. Find the root cause and fix it. Do not change the tests.",
1239
+ change: {
1240
+ "src/core/money.js": replaceIn(
1241
+ "src/core/money.js",
1242
+ "Math.round(amount * 100)",
1243
+ "Math.floor(amount * 100)"
1244
+ )
1245
+ },
1246
+ check: "node --test",
1247
+ solution: { "src/core/money.js": base("src/core/money.js") }
1248
+ }),
1249
+ task({
1250
+ id: "hard-event",
1251
+ title: "Fix a mismatch between two modules",
1252
+ prompt: "Creating an order no longer reserves stock. Find out why and fix it. Do not change the tests.",
1253
+ change: {
1254
+ "src/domains/stock/service.js": replaceIn(
1255
+ "src/domains/stock/service.js",
1256
+ '"order.created"',
1257
+ '"orders.created"'
1258
+ )
1259
+ },
1260
+ check: "node --test",
1261
+ solution: { "src/domains/stock/service.js": base("src/domains/stock/service.js") }
1262
+ }),
1263
+ task({
1264
+ id: "hard-import",
1265
+ title: "Fix a broken import in a large repo",
1266
+ prompt: "The API routes test crashes. Fix it. Do not change the tests.",
1267
+ change: {
1268
+ "src/api/routes.js": replaceIn(
1269
+ "src/api/routes.js",
1270
+ "domains/shipment/service.js",
1271
+ "domains/shipments/service.js"
1272
+ )
1273
+ },
1274
+ check: "node --test",
1275
+ solution: { "src/api/routes.js": base("src/api/routes.js") }
1276
+ }),
1277
+ task({
1278
+ id: "hard-rename",
1279
+ title: "Rename a shared function in every file",
1280
+ prompt: "Rename the function formatPrice (in src/core/money.js) to formatMoney: the definition and every import and call in src/. The tests already use the new name.",
1281
+ change: {
1282
+ "test/core/money.test.js": base("test/core/money.test.js").split("formatPrice").join("formatMoney")
1283
+ },
1284
+ check: "node --test && ! grep -rq formatPrice src",
1285
+ solution: renamed
1286
+ }),
1287
+ task({
1288
+ id: "hard-unused",
1289
+ title: "Find unused exports",
1290
+ prompt: "Which exported functions in src/core/ are not used by any other file in src/? Count only functions declared with the `function` keyword: classes and constants do not count. Uses in test/ do not count. Write the names to unused.txt, one per line, in alphabetical order. Do not change any other file.",
1291
+ change: {},
1292
+ // diff prints the expected and the actual lines, so a failure shows what the agent wrote.
1293
+ check: 'diff <(printf "isWeekend\\npadLeft\\ntruncate\\n") <(tr -d " \\r" < unused.txt | grep -v "^$")',
1294
+ protect: Object.keys(BASE).filter((p) => p.startsWith("src/")),
1295
+ solution: { "unused.txt": "isWeekend\npadLeft\ntruncate\n" }
1296
+ }),
1297
+ task({
1298
+ id: "hard-coupon",
1299
+ title: "Add a feature across modules",
1300
+ prompt: "Add coupon support to orders. When OrderService.create() gets a couponCode, find the coupon with that code and take its percent off the total (round to whole pence). An unknown code must throw a NotFoundError. test/features/coupon.test.js describes the behavior. Do not change the tests.",
1301
+ change: {
1302
+ "test/features/coupon.test.js": [
1303
+ 'import assert from "node:assert/strict";',
1304
+ 'import { test } from "node:test";',
1305
+ 'import { NotFoundError } from "../../src/core/errors.js";',
1306
+ 'import { EventBus } from "../../src/core/events.js";',
1307
+ 'import { CouponService } from "../../src/domains/coupon/service.js";',
1308
+ 'import { OrderService } from "../../src/domains/order/service.js";',
1309
+ 'import { ProductService } from "../../src/domains/product/service.js";',
1310
+ "",
1311
+ "function setup() {",
1312
+ " const bus = new EventBus();",
1313
+ " const products = new ProductService({ bus });",
1314
+ " const coupons = new CouponService({ bus });",
1315
+ ' coupons.create({ code: "SAVE10", percent: 10 });',
1316
+ ' const lamp = products.create({ name: "lamp", price: 19.99 });',
1317
+ " return { orders: new OrderService({ products, coupons, bus }), lamp };",
1318
+ "}",
1319
+ "",
1320
+ 'test("a coupon takes its percent off", () => {',
1321
+ " const { orders, lamp } = setup();",
1322
+ ' const order = orders.create({ customerId: "c1", couponCode: "SAVE10", lines: [{ productId: lamp.id, qty: 2 }] });',
1323
+ " assert.equal(order.totalCents, 3598);",
1324
+ "});",
1325
+ "",
1326
+ 'test("no coupon, full price", () => {',
1327
+ " const { orders, lamp } = setup();",
1328
+ ' assert.equal(orders.create({ customerId: "c1", lines: [{ productId: lamp.id, qty: 2 }] }).totalCents, 3998);',
1329
+ "});",
1330
+ "",
1331
+ 'test("an unknown code throws", () => {',
1332
+ " const { orders, lamp } = setup();",
1333
+ " assert.throws(",
1334
+ ' () => orders.create({ customerId: "c1", couponCode: "NOPE", lines: [{ productId: lamp.id, qty: 1 }] }),',
1335
+ " NotFoundError,",
1336
+ " );",
1337
+ "});",
1338
+ ""
1339
+ ].join("\n")
1340
+ },
1341
+ check: "node --test",
1342
+ solution: {
1343
+ "src/domains/order/service.js": base("src/domains/order/service.js").replace(
1344
+ "constructor({ products, repo = new OrderRepository(), bus = events } = {}) {\n this.products = products;",
1345
+ "constructor({ products, coupons, repo = new OrderRepository(), bus = events } = {}) {\n this.products = products;\n this.coupons = coupons;"
1346
+ ).replace("create({ customerId, lines }) {", "create({ customerId, lines, couponCode }) {").replace(
1347
+ " const order = this.repo.add(",
1348
+ [
1349
+ " if (couponCode !== undefined) {",
1350
+ " const coupon = this.coupons?.all().find((c) => c.code === couponCode);",
1351
+ " if (!coupon) throw new NotFoundError(`coupon ${couponCode}`);",
1352
+ " totalCents = Math.round(totalCents * (1 - coupon.percent / 100));",
1353
+ " }",
1354
+ " const order = this.repo.add("
1355
+ ].join("\n")
1356
+ )
1357
+ }
1358
+ })
1359
+ ];
1360
+
1361
+ // src/evals/javaTasks.ts
1362
+ var java = (pkg2, ...body) => lines(`package ${pkg2};`, "", ...body);
1363
+ var junit = (pkg2, ...body) => java(
1364
+ pkg2,
1365
+ "import static org.junit.jupiter.api.Assertions.*;",
1366
+ "",
1367
+ "import org.junit.jupiter.api.Test;",
1368
+ ...body
1369
+ );
1370
+ var main = (path) => `src/main/java/${path}`;
1371
+ var test = (path) => `src/test/java/${path}`;
1372
+ var task2 = (t) => ({
1373
+ ...t,
1374
+ files: { "pom.xml": MAVEN_POM, ...t.files },
1375
+ check: MAVEN_CHECK,
1376
+ requires: ["maven"]
1377
+ });
1378
+ var paginator = (end) => java(
1379
+ "app",
1380
+ "import java.util.List;",
1381
+ "",
1382
+ "public final class Paginator {",
1383
+ " private Paginator() {}",
1384
+ "",
1385
+ " /** Page numbers start at 1. A page after the last one is empty. */",
1386
+ " public static <T> List<T> page(List<T> items, int page, int size) {",
1387
+ " if (page < 1 || size < 1) {",
1388
+ ' throw new IllegalArgumentException("page and size must be positive");',
1389
+ " }",
1390
+ " int from = (page - 1) * size;",
1391
+ " if (from >= items.size()) {",
1392
+ " return List.of();",
1393
+ " }",
1394
+ ` int to = ${end};`,
1395
+ " return items.subList(from, to);",
1396
+ " }",
1397
+ "}"
1398
+ );
1399
+ var money = (...extra) => java(
1400
+ "app",
1401
+ "import java.math.BigDecimal;",
1402
+ ...extra.length > 0 ? ["import java.util.Objects;"] : [],
1403
+ "",
1404
+ "/** An amount of money. 1.5 GBP and 1.50 GBP are equal. */",
1405
+ "public final class Money {",
1406
+ " private final BigDecimal amount;",
1407
+ " private final String currency;",
1408
+ "",
1409
+ " public Money(String amount, String currency) {",
1410
+ " this.amount = new BigDecimal(amount);",
1411
+ " this.currency = currency;",
1412
+ " }",
1413
+ "",
1414
+ " @Override",
1415
+ " public boolean equals(Object other) {",
1416
+ " if (this == other) {",
1417
+ " return true;",
1418
+ " }",
1419
+ " if (!(other instanceof Money m)) {",
1420
+ " return false;",
1421
+ " }",
1422
+ " return amount.compareTo(m.amount) == 0 && currency.equals(m.currency);",
1423
+ " }",
1424
+ ...extra,
1425
+ "",
1426
+ " @Override",
1427
+ " public String toString() {",
1428
+ ' return amount.toPlainString() + " " + currency;',
1429
+ " }",
1430
+ "}"
1431
+ );
1432
+ var repo = (name) => java(
1433
+ "app",
1434
+ "import java.util.Map;",
1435
+ "",
1436
+ "public final class UserRepo {",
1437
+ ' private final Map<Integer, String> names = Map.of(1, "ada", 2, "linus");',
1438
+ "",
1439
+ ` public String ${name}(int id) {`,
1440
+ ' return names.getOrDefault(id, "unknown");',
1441
+ " }",
1442
+ "}"
1443
+ );
1444
+ var profile = (name) => java(
1445
+ "app",
1446
+ "public final class ProfileService {",
1447
+ " private final UserRepo repo;",
1448
+ "",
1449
+ " public ProfileService(UserRepo repo) {",
1450
+ " this.repo = repo;",
1451
+ " }",
1452
+ "",
1453
+ " public String title(int id) {",
1454
+ ` return "Profile of " + repo.${name}(id);`,
1455
+ " }",
1456
+ "}"
1457
+ );
1458
+ var admin = (name) => java(
1459
+ "app",
1460
+ "public final class AdminService {",
1461
+ " private final UserRepo repo;",
1462
+ "",
1463
+ " public AdminService(UserRepo repo) {",
1464
+ " this.repo = repo;",
1465
+ " }",
1466
+ "",
1467
+ " public boolean isAdmin(int id) {",
1468
+ ` return "ada".equals(repo.${name}(id));`,
1469
+ " }",
1470
+ "}"
1471
+ );
1472
+ var report = (body, imports2) => java(
1473
+ "shop",
1474
+ ...imports2,
1475
+ "",
1476
+ "public final class OrderReport {",
1477
+ " private OrderReport() {}",
1478
+ "",
1479
+ " /** Total per customer, in customer name order. Cancelled orders do not count. */",
1480
+ " public static Map<String, BigDecimal> totalsByCustomer(List<Order> orders) {",
1481
+ ...body,
1482
+ " }",
1483
+ "}"
1484
+ );
1485
+ var JAVA_TASKS = [
1486
+ task2({
1487
+ id: "java-paginate",
1488
+ title: "Java: fix the last page of a paginator",
1489
+ prompt: "The tests of this Maven project fail: the last page of Paginator is wrong. Fix the bug. Do not change the tests.",
1490
+ files: {
1491
+ [main("app/Paginator.java")]: paginator("Math.min(from + size, items.size() - 1)"),
1492
+ [test("app/PaginatorTest.java")]: junit(
1493
+ "app",
1494
+ "import java.util.List;",
1495
+ "",
1496
+ "class PaginatorTest {",
1497
+ " private final List<Integer> items = List.of(1, 2, 3, 4, 5);",
1498
+ "",
1499
+ " @Test",
1500
+ " void fullPages() {",
1501
+ " assertEquals(List.of(1, 2), Paginator.page(items, 1, 2));",
1502
+ " assertEquals(List.of(3, 4), Paginator.page(items, 2, 2));",
1503
+ " }",
1504
+ "",
1505
+ " @Test",
1506
+ " void lastPageMayBeShort() {",
1507
+ " assertEquals(List.of(5), Paginator.page(items, 3, 2));",
1508
+ " assertEquals(items, Paginator.page(items, 1, 10));",
1509
+ " }",
1510
+ "",
1511
+ " @Test",
1512
+ " void pageAfterTheEndIsEmpty() {",
1513
+ " assertEquals(List.of(), Paginator.page(items, 4, 2));",
1514
+ " }",
1515
+ "",
1516
+ " @Test",
1517
+ " void rejectsBadArguments() {",
1518
+ " assertThrows(IllegalArgumentException.class, () -> Paginator.page(items, 0, 2));",
1519
+ " }",
1520
+ "}"
1521
+ )
1522
+ },
1523
+ solution: { [main("app/Paginator.java")]: paginator("Math.min(from + size, items.size())") }
1524
+ }),
1525
+ task2({
1526
+ id: "java-hashcode",
1527
+ title: "Java: make Money work in a HashSet",
1528
+ prompt: "Money objects that are equal do not work as keys in a HashSet or HashMap, and the tests fail. Fix Money. Do not change the tests.",
1529
+ files: {
1530
+ [main("app/Money.java")]: money(),
1531
+ [test("app/MoneyTest.java")]: junit(
1532
+ "app",
1533
+ "import java.util.HashMap;",
1534
+ "import java.util.HashSet;",
1535
+ "import java.util.Map;",
1536
+ "import java.util.Set;",
1537
+ "",
1538
+ "class MoneyTest {",
1539
+ " @Test",
1540
+ " void equalAmountsAreEqual() {",
1541
+ ' assertEquals(new Money("1.5", "GBP"), new Money("1.50", "GBP"));',
1542
+ ' assertNotEquals(new Money("1.5", "GBP"), new Money("1.5", "EUR"));',
1543
+ " }",
1544
+ "",
1545
+ " @Test",
1546
+ " void equalMoneyIsOneSetElement() {",
1547
+ " Set<Money> set = new HashSet<>();",
1548
+ ' set.add(new Money("1.50", "GBP"));',
1549
+ ' set.add(new Money("1.5", "GBP"));',
1550
+ " assertEquals(1, set.size());",
1551
+ ' assertTrue(set.contains(new Money("1.500", "GBP")));',
1552
+ " }",
1553
+ "",
1554
+ " @Test",
1555
+ " void worksAsAMapKey() {",
1556
+ " Map<Money, String> prices = new HashMap<>();",
1557
+ ' prices.put(new Money("2", "EUR"), "tea");',
1558
+ ' assertEquals("tea", prices.get(new Money("2.00", "EUR")));',
1559
+ ' assertNull(prices.get(new Money("2", "GBP")));',
1560
+ " }",
1561
+ "}"
1562
+ )
1563
+ },
1564
+ solution: {
1565
+ [main("app/Money.java")]: money(
1566
+ "",
1567
+ " @Override",
1568
+ " public int hashCode() {",
1569
+ " return Objects.hash(amount.stripTrailingZeros(), currency);",
1570
+ " }"
1571
+ )
1572
+ }
1573
+ }),
1574
+ task2({
1575
+ id: "java-rename",
1576
+ title: "Java: rename a method in every class",
1577
+ prompt: "Rename the method findUsr of UserRepo to findUser everywhere in src/main/java (the definition and every call). Then run the tests.",
1578
+ files: {
1579
+ [main("app/UserRepo.java")]: repo("findUsr"),
1580
+ [main("app/ProfileService.java")]: profile("findUsr"),
1581
+ [main("app/AdminService.java")]: admin("findUsr"),
1582
+ [test("app/ServicesTest.java")]: junit(
1583
+ "app",
1584
+ "class ServicesTest {",
1585
+ " private final UserRepo repo = new UserRepo();",
1586
+ "",
1587
+ " @Test",
1588
+ " void repoFindsUsers() {",
1589
+ ' assertEquals("ada", repo.findUser(1));',
1590
+ ' assertEquals("unknown", repo.findUser(9));',
1591
+ " }",
1592
+ "",
1593
+ " @Test",
1594
+ " void servicesUseTheRepo() {",
1595
+ ' assertEquals("Profile of linus", new ProfileService(repo).title(2));',
1596
+ " assertTrue(new AdminService(repo).isAdmin(1));",
1597
+ " }",
1598
+ "}"
1599
+ )
1600
+ },
1601
+ solution: {
1602
+ [main("app/UserRepo.java")]: repo("findUser"),
1603
+ [main("app/ProfileService.java")]: profile("findUser"),
1604
+ [main("app/AdminService.java")]: admin("findUser")
1605
+ }
1606
+ }),
1607
+ task2({
1608
+ id: "java-lru-cache",
1609
+ title: "Java: implement an LRU cache from its tests",
1610
+ prompt: "Implement LruCache so that its tests pass. Read the tests first. Do not change the tests.",
1611
+ files: {
1612
+ [main("cache/LruCache.java")]: java(
1613
+ "cache",
1614
+ "/** A cache with a fixed capacity. When it is full, put removes the least recently used entry. */",
1615
+ "public final class LruCache<K, V> {",
1616
+ " public LruCache(int capacity) {",
1617
+ ' throw new UnsupportedOperationException("not implemented");',
1618
+ " }",
1619
+ "",
1620
+ " /** The value, or null. A hit makes the entry the most recently used. */",
1621
+ " public V get(K key) {",
1622
+ ' throw new UnsupportedOperationException("not implemented");',
1623
+ " }",
1624
+ "",
1625
+ " public void put(K key, V value) {",
1626
+ ' throw new UnsupportedOperationException("not implemented");',
1627
+ " }",
1628
+ "",
1629
+ " public int size() {",
1630
+ ' throw new UnsupportedOperationException("not implemented");',
1631
+ " }",
1632
+ "}"
1633
+ ),
1634
+ [test("cache/LruCacheTest.java")]: junit(
1635
+ "cache",
1636
+ "class LruCacheTest {",
1637
+ " @Test",
1638
+ " void evictsTheLeastRecentlyUsedEntry() {",
1639
+ " LruCache<String, Integer> cache = new LruCache<>(2);",
1640
+ ' cache.put("a", 1);',
1641
+ ' cache.put("b", 2);',
1642
+ ' cache.put("c", 3);',
1643
+ ' assertNull(cache.get("a"));',
1644
+ ' assertEquals(2, cache.get("b"));',
1645
+ ' assertEquals(3, cache.get("c"));',
1646
+ " assertEquals(2, cache.size());",
1647
+ " }",
1648
+ "",
1649
+ " @Test",
1650
+ " void getMakesAnEntryRecent() {",
1651
+ " LruCache<String, Integer> cache = new LruCache<>(2);",
1652
+ ' cache.put("a", 1);',
1653
+ ' cache.put("b", 2);',
1654
+ ' cache.get("a");',
1655
+ ' cache.put("c", 3);',
1656
+ ' assertEquals(1, cache.get("a"));',
1657
+ ' assertNull(cache.get("b"));',
1658
+ " }",
1659
+ "",
1660
+ " @Test",
1661
+ " void putReplacesAValueAndMakesItRecent() {",
1662
+ " LruCache<String, Integer> cache = new LruCache<>(2);",
1663
+ ' cache.put("a", 1);',
1664
+ ' cache.put("b", 2);',
1665
+ ' cache.put("a", 10);',
1666
+ ' cache.put("c", 3);',
1667
+ ' assertEquals(10, cache.get("a"));',
1668
+ ' assertNull(cache.get("b"));',
1669
+ " assertEquals(2, cache.size());",
1670
+ " }",
1671
+ "",
1672
+ " @Test",
1673
+ " void rejectsAnEmptyCapacity() {",
1674
+ " assertThrows(IllegalArgumentException.class, () -> new LruCache<String, Integer>(0));",
1675
+ " }",
1676
+ "}"
1677
+ )
1678
+ },
1679
+ solution: {
1680
+ [main("cache/LruCache.java")]: java(
1681
+ "cache",
1682
+ "import java.util.LinkedHashMap;",
1683
+ "import java.util.Map;",
1684
+ "",
1685
+ "public final class LruCache<K, V> {",
1686
+ " private final Map<K, V> map;",
1687
+ "",
1688
+ " public LruCache(int capacity) {",
1689
+ " if (capacity < 1) {",
1690
+ ' throw new IllegalArgumentException("capacity must be positive");',
1691
+ " }",
1692
+ " this.map = new LinkedHashMap<>(16, 0.75f, true) {",
1693
+ " @Override",
1694
+ " protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {",
1695
+ " return size() > capacity;",
1696
+ " }",
1697
+ " };",
1698
+ " }",
1699
+ "",
1700
+ " public V get(K key) {",
1701
+ " return map.get(key);",
1702
+ " }",
1703
+ "",
1704
+ " public void put(K key, V value) {",
1705
+ " map.put(key, value);",
1706
+ " }",
1707
+ "",
1708
+ " public int size() {",
1709
+ " return map.size();",
1710
+ " }",
1711
+ "}"
1712
+ )
1713
+ }
1714
+ }),
1715
+ task2({
1716
+ id: "java-report",
1717
+ title: "Java: fix a stream report with two bugs",
1718
+ prompt: "OrderReport.totalsByCustomer is wrong: cancelled orders must not count, and customers must be in name order. The tests fail. Fix OrderReport. Do not change the tests or Order.",
1719
+ protect: ["pom.xml", test("shop/OrderReportTest.java"), main("shop/Order.java")],
1720
+ files: {
1721
+ [main("shop/Order.java")]: java(
1722
+ "shop",
1723
+ "import java.math.BigDecimal;",
1724
+ "",
1725
+ "public record Order(String customer, BigDecimal total, Status status) {",
1726
+ " public enum Status {",
1727
+ " OPEN,",
1728
+ " PAID,",
1729
+ " CANCELLED",
1730
+ " }",
1731
+ "}"
1732
+ ),
1733
+ [main("shop/OrderReport.java")]: report(
1734
+ [
1735
+ " Map<String, BigDecimal> totals = new HashMap<>();",
1736
+ " for (Order order : orders) {",
1737
+ " totals.merge(order.customer(), order.total(), BigDecimal::add);",
1738
+ " }",
1739
+ " return totals;"
1740
+ ],
1741
+ [
1742
+ "import java.math.BigDecimal;",
1743
+ "import java.util.HashMap;",
1744
+ "import java.util.List;",
1745
+ "import java.util.Map;"
1746
+ ]
1747
+ ),
1748
+ [test("shop/OrderReportTest.java")]: junit(
1749
+ "shop",
1750
+ "import java.math.BigDecimal;",
1751
+ "import java.util.ArrayList;",
1752
+ "import java.util.List;",
1753
+ "import java.util.Map;",
1754
+ "",
1755
+ "class OrderReportTest {",
1756
+ " private static Order order(String customer, String total, Order.Status status) {",
1757
+ " return new Order(customer, new BigDecimal(total), status);",
1758
+ " }",
1759
+ "",
1760
+ " private final List<Order> orders =",
1761
+ " List.of(",
1762
+ ' order("zoe", "10.00", Order.Status.PAID),',
1763
+ ' order("mia", "2.50", Order.Status.OPEN),',
1764
+ ' order("zoe", "5.25", Order.Status.CANCELLED),',
1765
+ ' order("adam", "7.10", Order.Status.PAID),',
1766
+ ' order("mia", "0.40", Order.Status.PAID),',
1767
+ ' order("bob", "3.00", Order.Status.CANCELLED));',
1768
+ "",
1769
+ " @Test",
1770
+ " void skipsCancelledOrders() {",
1771
+ " Map<String, BigDecimal> totals = OrderReport.totalsByCustomer(orders);",
1772
+ ' assertEquals(0, new BigDecimal("10.00").compareTo(totals.get("zoe")));',
1773
+ ' assertEquals(0, new BigDecimal("2.90").compareTo(totals.get("mia")));',
1774
+ ' assertFalse(totals.containsKey("bob"));',
1775
+ " }",
1776
+ "",
1777
+ " @Test",
1778
+ " void listsCustomersInNameOrder() {",
1779
+ " Map<String, BigDecimal> totals = OrderReport.totalsByCustomer(orders);",
1780
+ ' assertEquals(List.of("adam", "mia", "zoe"), new ArrayList<>(totals.keySet()));',
1781
+ " }",
1782
+ "",
1783
+ " @Test",
1784
+ " void anEmptyListGivesAnEmptyReport() {",
1785
+ " assertTrue(OrderReport.totalsByCustomer(List.of()).isEmpty());",
1786
+ " }",
1787
+ "}"
1788
+ )
1789
+ },
1790
+ solution: {
1791
+ [main("shop/OrderReport.java")]: report(
1792
+ [
1793
+ " return orders.stream()",
1794
+ " .filter(order -> order.status() != Order.Status.CANCELLED)",
1795
+ " .collect(",
1796
+ " Collectors.groupingBy(",
1797
+ " Order::customer,",
1798
+ " TreeMap::new,",
1799
+ " Collectors.reducing(BigDecimal.ZERO, Order::total, BigDecimal::add)));"
1800
+ ],
1801
+ [
1802
+ "import java.math.BigDecimal;",
1803
+ "import java.util.List;",
1804
+ "import java.util.Map;",
1805
+ "import java.util.TreeMap;",
1806
+ "import java.util.stream.Collectors;"
1807
+ ]
1808
+ )
1809
+ }
1810
+ })
1811
+ ];
1812
+
1813
+ // src/evals/pythonTasks.ts
1814
+ var task3 = (t) => ({
1815
+ ...t,
1816
+ files: { "pyproject.toml": PYPROJECT, ...t.files },
1817
+ check: PYTEST_CHECK,
1818
+ requires: ["pytest"]
1819
+ });
1820
+ var cart = (init, body) => lines(
1821
+ "class Cart:",
1822
+ ' """A shopping cart. Each cart has its own items."""',
1823
+ "",
1824
+ ` def __init__(self, items=${init}):`,
1825
+ ` ${body}`,
1826
+ "",
1827
+ " def add(self, item):",
1828
+ " self.items.append(item)",
1829
+ "",
1830
+ " def count(self):",
1831
+ " return len(self.items)"
1832
+ );
1833
+ var chunks = (stop) => lines(
1834
+ "def chunks(seq, size):",
1835
+ ' """Split seq into lists of `size` items. The last list may be shorter."""',
1836
+ " if size < 1:",
1837
+ ' raise ValueError("size must be positive")',
1838
+ ` return [list(seq[i : i + size]) for i in range(0, ${stop}, size)]`
1839
+ );
1840
+ var users = (name) => lines(
1841
+ 'NAMES = {1: "ada", 2: "guido"}',
1842
+ "",
1843
+ "",
1844
+ `def ${name}(user_id):`,
1845
+ ' return NAMES.get(user_id, "unknown")'
1846
+ );
1847
+ var profile2 = (name) => lines(
1848
+ `from app.users import ${name}`,
1849
+ "",
1850
+ "",
1851
+ "def title(user_id):",
1852
+ ` return f"Profile of {${name}(user_id)}"`
1853
+ );
1854
+ var admin2 = (name) => lines(
1855
+ "from app import users",
1856
+ "",
1857
+ "",
1858
+ "def is_admin(user_id):",
1859
+ ` return users.${name}(user_id) == "ada"`
1860
+ );
1861
+ var top = (key, reverse) => lines(
1862
+ "def top_players(scores, n):",
1863
+ ' """The n best players: highest score first; equal scores in name order."""',
1864
+ ` return sorted(scores, key=${key}${reverse})[:n]`
1865
+ );
1866
+ var PYTHON_TASKS = [
1867
+ task3({
1868
+ id: "py-mutable-default",
1869
+ title: "Python: carts share their items",
1870
+ prompt: "Two Cart objects share the same items, and a test fails. Fix Cart. Do not change the tests.",
1871
+ files: {
1872
+ "src/shop/__init__.py": "",
1873
+ "src/shop/cart.py": cart("[]", "self.items = items"),
1874
+ "tests/test_cart.py": lines(
1875
+ "from shop.cart import Cart",
1876
+ "",
1877
+ "",
1878
+ "def test_new_carts_are_empty_and_separate():",
1879
+ " first = Cart()",
1880
+ ' first.add("tea")',
1881
+ " second = Cart()",
1882
+ " assert second.count() == 0",
1883
+ " assert first.count() == 1",
1884
+ "",
1885
+ "",
1886
+ "def test_a_cart_can_start_with_items():",
1887
+ ' items = ["milk"]',
1888
+ " cart = Cart(items)",
1889
+ ' cart.add("bread")',
1890
+ " assert cart.count() == 2"
1891
+ )
1892
+ },
1893
+ solution: {
1894
+ "src/shop/cart.py": cart("None", "self.items = [] if items is None else items")
1895
+ }
1896
+ }),
1897
+ task3({
1898
+ id: "py-chunks",
1899
+ title: "Python: keep the last short chunk",
1900
+ prompt: "chunks() loses the last items when they do not fill a whole chunk. Fix it without changing the tests.",
1901
+ files: {
1902
+ "src/listutil/__init__.py": "from listutil.chunks import chunks\n",
1903
+ "src/listutil/chunks.py": chunks("len(seq) - size + 1"),
1904
+ "tests/test_chunks.py": lines(
1905
+ "import pytest",
1906
+ "",
1907
+ "from listutil import chunks",
1908
+ "",
1909
+ "",
1910
+ "def test_even_split():",
1911
+ " assert chunks([1, 2, 3, 4], 2) == [[1, 2], [3, 4]]",
1912
+ "",
1913
+ "",
1914
+ "def test_last_chunk_may_be_short():",
1915
+ " assert chunks([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]]",
1916
+ ' assert chunks("abc", 5) == [["a", "b", "c"]]',
1917
+ "",
1918
+ "",
1919
+ "def test_empty_input():",
1920
+ " assert chunks([], 3) == []",
1921
+ "",
1922
+ "",
1923
+ "def test_rejects_a_bad_size():",
1924
+ " with pytest.raises(ValueError):",
1925
+ " chunks([1], 0)"
1926
+ )
1927
+ },
1928
+ solution: { "src/listutil/chunks.py": chunks("len(seq)") }
1929
+ }),
1930
+ task3({
1931
+ id: "py-rename",
1932
+ title: "Python: rename a function in every module",
1933
+ prompt: "Rename the function get_usr to get_user everywhere in src/ (the definition and every use). Then run the tests.",
1934
+ files: {
1935
+ "src/app/__init__.py": "",
1936
+ "src/app/users.py": users("get_usr"),
1937
+ "src/app/profile.py": profile2("get_usr"),
1938
+ "src/app/admin.py": admin2("get_usr"),
1939
+ "tests/test_app.py": lines(
1940
+ "from app.admin import is_admin",
1941
+ "from app.profile import title",
1942
+ "from app.users import get_user",
1943
+ "",
1944
+ "",
1945
+ "def test_users():",
1946
+ ' assert get_user(1) == "ada"',
1947
+ ' assert get_user(9) == "unknown"',
1948
+ "",
1949
+ "",
1950
+ "def test_services():",
1951
+ ' assert title(2) == "Profile of guido"',
1952
+ " assert is_admin(1)"
1953
+ )
1954
+ },
1955
+ solution: {
1956
+ "src/app/users.py": users("get_user"),
1957
+ "src/app/profile.py": profile2("get_user"),
1958
+ "src/app/admin.py": admin2("get_user")
1959
+ }
1960
+ }),
1961
+ task3({
1962
+ id: "py-duration",
1963
+ title: "Python: implement a duration parser from its tests",
1964
+ prompt: "Implement parse_duration in src/timeparse/duration.py so that the tests pass. Read the tests first. Do not change the tests.",
1965
+ files: {
1966
+ "src/timeparse/__init__.py": "",
1967
+ "src/timeparse/duration.py": lines(
1968
+ "def parse_duration(text):",
1969
+ ' """Seconds in a duration such as "1h30m", "45s" or "2h5s"."""',
1970
+ ' raise NotImplementedError("parse_duration")'
1971
+ ),
1972
+ "tests/test_duration.py": lines(
1973
+ "import pytest",
1974
+ "",
1975
+ "from timeparse.duration import parse_duration",
1976
+ "",
1977
+ "",
1978
+ '@pytest.mark.parametrize("text, seconds", [',
1979
+ ' ("45s", 45),',
1980
+ ' ("2m", 120),',
1981
+ ' ("1h30m", 5400),',
1982
+ ' ("2h5s", 7205),',
1983
+ ' ("1h1m1s", 3661),',
1984
+ ' ("0s", 0),',
1985
+ "])",
1986
+ "def test_valid(text, seconds):",
1987
+ " assert parse_duration(text) == seconds",
1988
+ "",
1989
+ "",
1990
+ '@pytest.mark.parametrize("text", ["", "10", "5x", "1m1h", "1h1h", "h", "1 h", "-1s"])',
1991
+ "def test_invalid(text):",
1992
+ " with pytest.raises(ValueError):",
1993
+ " parse_duration(text)"
1994
+ )
1995
+ },
1996
+ solution: {
1997
+ "src/timeparse/duration.py": lines(
1998
+ "import re",
1999
+ "",
2000
+ 'PATTERN = re.compile(r"(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?")',
2001
+ "",
2002
+ "",
2003
+ "def parse_duration(text):",
2004
+ " match = PATTERN.fullmatch(text)",
2005
+ " if not text or match is None:",
2006
+ ' raise ValueError(f"not a duration: {text!r}")',
2007
+ " hours, minutes, seconds = (int(g) if g else 0 for g in match.groups())",
2008
+ " return hours * 3600 + minutes * 60 + seconds"
2009
+ )
2010
+ }
2011
+ }),
2012
+ task3({
2013
+ id: "py-top-players",
2014
+ title: "Python: fix the order of equal scores",
2015
+ prompt: "top_players returns players with equal scores in the wrong order, and a test fails. Fix it without changing the tests.",
2016
+ files: {
2017
+ "src/league/__init__.py": "",
2018
+ "src/league/ranking.py": top("scores.get", ", reverse=True"),
2019
+ "tests/test_ranking.py": lines(
2020
+ "from league.ranking import top_players",
2021
+ "",
2022
+ 'SCORES = {"zed": 5, "amy": 5, "bob": 7, "cat": 1}',
2023
+ "",
2024
+ "",
2025
+ "def test_best_first_and_ties_in_name_order():",
2026
+ ' assert top_players(SCORES, 3) == ["bob", "amy", "zed"]',
2027
+ "",
2028
+ "",
2029
+ "def test_n_larger_than_the_list():",
2030
+ ' assert top_players(SCORES, 10) == ["bob", "amy", "zed", "cat"]',
2031
+ "",
2032
+ "",
2033
+ "def test_zero():",
2034
+ " assert top_players(SCORES, 0) == []"
2035
+ )
2036
+ },
2037
+ solution: {
2038
+ "src/league/ranking.py": top("lambda name: (-scores[name], name)", "")
2039
+ }
2040
+ })
2041
+ ];
2042
+
2043
+ // src/evals/tasks.ts
2044
+ var pkg = '{ "name": "eval", "type": "module", "private": true }\n';
2045
+ var lines3 = (...l) => `${l.join("\n")}
2046
+ `;
2047
+ var imports = (from, names) => lines3(
2048
+ 'import assert from "node:assert/strict";',
2049
+ 'import { test } from "node:test";',
2050
+ `import { ${names} } from "${from}";`
2051
+ );
2052
+ var EVAL_TASKS = [
2053
+ {
2054
+ id: "fix-add",
2055
+ title: "Fix a wrong operator",
2056
+ prompt: "The tests fail. Find the bug and fix it. Do not change the tests.",
2057
+ files: {
2058
+ "package.json": pkg,
2059
+ "src/math.js": lines3("export function add(a, b) {", " return a - b;", "}"),
2060
+ "test/math.test.js": `${imports("../src/math.js", "add")}test("add", () => assert.equal(add(2, 3), 5));
2061
+ `
2062
+ },
2063
+ check: "node --test",
2064
+ solution: { "src/math.js": lines3("export function add(a, b) {", " return a + b;", "}") }
2065
+ },
2066
+ {
2067
+ id: "off-by-one",
2068
+ title: "Fix an off-by-one error",
2069
+ prompt: "range(n) should return [0, 1, \u2026, n-1], but a test fails. Fix it without changing the tests.",
2070
+ files: {
2071
+ "package.json": pkg,
2072
+ "src/range.js": lines3(
2073
+ "export function range(n) {",
2074
+ " const out = [];",
2075
+ " for (let i = 0; i < n - 1; i++) out.push(i);",
2076
+ " return out;",
2077
+ "}"
2078
+ ),
2079
+ "test/range.test.js": `${imports("../src/range.js", "range")}${lines3(
2080
+ 'test("range", () => {',
2081
+ " assert.deepEqual(range(3), [0, 1, 2]);",
2082
+ " assert.deepEqual(range(0), []);",
2083
+ "});"
2084
+ )}`
2085
+ },
2086
+ check: "node --test",
2087
+ solution: {
2088
+ "src/range.js": lines3(
2089
+ "export function range(n) {",
2090
+ " const out = [];",
2091
+ " for (let i = 0; i < n; i++) out.push(i);",
2092
+ " return out;",
2093
+ "}"
2094
+ )
2095
+ }
2096
+ },
2097
+ {
2098
+ id: "rename",
2099
+ title: "Rename a function in every file",
2100
+ prompt: "Rename the function getUsr to getUser everywhere in src/ (definition and every use). Then run the tests.",
2101
+ files: {
2102
+ "package.json": pkg,
2103
+ "src/users.js": lines3(
2104
+ "export function getUsr(id) {",
2105
+ " return { id, name: `user-${id}` };",
2106
+ "}"
2107
+ ),
2108
+ "src/profile.js": lines3(
2109
+ 'import { getUsr } from "./users.js";',
2110
+ "export const profile = (id) => `Profile of ${getUsr(id).name}`;"
2111
+ ),
2112
+ "src/admin.js": lines3(
2113
+ 'import { getUsr } from "./users.js";',
2114
+ "export const isAdmin = (id) => getUsr(id).id === 1;"
2115
+ ),
2116
+ "test/users.test.js": `${imports("../src/users.js", "getUser")}${lines3(
2117
+ 'import { profile } from "../src/profile.js";',
2118
+ 'import { isAdmin } from "../src/admin.js";',
2119
+ 'test("rename", () => {',
2120
+ ' assert.equal(getUser(2).name, "user-2");',
2121
+ ' assert.equal(profile(3), "Profile of user-3");',
2122
+ " assert.equal(isAdmin(1), true);",
2123
+ "});"
2124
+ )}`
2125
+ },
2126
+ check: "node --test && ! grep -rq getUsr src",
2127
+ solution: {
2128
+ "src/users.js": lines3(
2129
+ "export function getUser(id) {",
2130
+ " return { id, name: `user-${id}` };",
2131
+ "}"
2132
+ ),
2133
+ "src/profile.js": lines3(
2134
+ 'import { getUser } from "./users.js";',
2135
+ "export const profile = (id) => `Profile of ${getUser(id).name}`;"
2136
+ ),
2137
+ "src/admin.js": lines3(
2138
+ 'import { getUser } from "./users.js";',
2139
+ "export const isAdmin = (id) => getUser(id).id === 1;"
2140
+ )
2141
+ }
2142
+ },
2143
+ {
2144
+ id: "implement-slugify",
2145
+ title: "Implement a function from its tests",
2146
+ prompt: "Implement slugify in src/slugify.js so that the tests pass.",
2147
+ files: {
2148
+ "package.json": pkg,
2149
+ "src/slugify.js": lines3(
2150
+ "export function slugify(text) {",
2151
+ ' throw new Error("not implemented");',
2152
+ "}"
2153
+ ),
2154
+ "test/slugify.test.js": `${imports("../src/slugify.js", "slugify")}${lines3(
2155
+ 'test("slugify", () => {',
2156
+ ' assert.equal(slugify("Hello World"), "hello-world");',
2157
+ ' assert.equal(slugify(" Many spaces "), "many-spaces");',
2158
+ ' assert.equal(slugify("Caf\xE9 & Cr\xE8me!"), "cafe-creme");',
2159
+ ' assert.equal(slugify("a--b"), "a-b");',
2160
+ "});"
2161
+ )}`
2162
+ },
2163
+ check: "node --test",
2164
+ solution: {
2165
+ "src/slugify.js": lines3(
2166
+ "export function slugify(text) {",
2167
+ " return text",
2168
+ ' .normalize("NFKD")',
2169
+ ' .replace(/[\\u0300-\\u036f]/g, "")',
2170
+ " .toLowerCase()",
2171
+ ' .replace(/[^a-z0-9]+/g, "-")',
2172
+ ' .replace(/^-+|-+$/g, "");',
2173
+ "}"
2174
+ )
2175
+ }
2176
+ },
2177
+ {
2178
+ id: "missing-await",
2179
+ title: "Fix a missing await",
2180
+ prompt: "loadTotal sometimes returns the wrong value and its test fails. Find the cause and fix it.",
2181
+ files: {
2182
+ "package.json": pkg,
2183
+ "src/total.js": lines3(
2184
+ "const fetchPrices = async () => {",
2185
+ " await new Promise((r) => setTimeout(r, 5));",
2186
+ " return [3, 4, 5];",
2187
+ "};",
2188
+ "",
2189
+ "export async function loadTotal() {",
2190
+ " const prices = fetchPrices();",
2191
+ " let total = 0;",
2192
+ " for (const p of prices ?? []) total += p;",
2193
+ " return total;",
2194
+ "}"
2195
+ ),
2196
+ "test/total.test.js": `${imports("../src/total.js", "loadTotal")}test("total", async () => assert.equal(await loadTotal(), 12));
2197
+ `
2198
+ },
2199
+ check: "node --test",
2200
+ solution: {
2201
+ "src/total.js": lines3(
2202
+ "const fetchPrices = async () => {",
2203
+ " await new Promise((r) => setTimeout(r, 5));",
2204
+ " return [3, 4, 5];",
2205
+ "};",
2206
+ "",
2207
+ "export async function loadTotal() {",
2208
+ " const prices = await fetchPrices();",
2209
+ " let total = 0;",
2210
+ " for (const p of prices ?? []) total += p;",
2211
+ " return total;",
2212
+ "}"
2213
+ )
2214
+ }
2215
+ },
2216
+ {
2217
+ id: "csv-quotes",
2218
+ title: "Handle quoted fields in a CSV parser",
2219
+ prompt: 'parseLine splits a CSV line at commas. It must also handle fields in double quotes that contain commas and escaped quotes (""). Fix it so the tests pass.',
2220
+ files: {
2221
+ "package.json": pkg,
2222
+ "src/csv.js": lines3("export function parseLine(line) {", ' return line.split(",");', "}"),
2223
+ "test/csv.test.js": `${imports("../src/csv.js", "parseLine")}${lines3(
2224
+ 'test("plain", () => assert.deepEqual(parseLine("a,b,c"), ["a", "b", "c"]));',
2225
+ `test("quoted comma", () => assert.deepEqual(parseLine('x,"a, b",y'), ["x", "a, b", "y"]));`,
2226
+ `test("escaped quote", () => assert.deepEqual(parseLine('"say ""hi""",z'), ['say "hi"', "z"]));`,
2227
+ 'test("empty fields", () => assert.deepEqual(parseLine("a,,b"), ["a", "", "b"]));'
2228
+ )}`
2229
+ },
2230
+ check: "node --test",
2231
+ solution: {
2232
+ "src/csv.js": lines3(
2233
+ "export function parseLine(line) {",
2234
+ " const out = [];",
2235
+ ' let field = "";',
2236
+ " let quoted = false;",
2237
+ " for (let i = 0; i < line.length; i++) {",
2238
+ " const c = line[i];",
2239
+ " if (quoted) {",
2240
+ ` if (c === '"' && line[i + 1] === '"') { field += '"'; i++; }`,
2241
+ ` else if (c === '"') quoted = false;`,
2242
+ " else field += c;",
2243
+ ` } else if (c === '"') quoted = true;`,
2244
+ ' else if (c === ",") { out.push(field); field = ""; }',
2245
+ " else field += c;",
2246
+ " }",
2247
+ " out.push(field);",
2248
+ " return out;",
2249
+ "}"
2250
+ )
2251
+ }
2252
+ },
2253
+ {
2254
+ id: "write-tests",
2255
+ title: "Write tests for existing code",
2256
+ prompt: "Write a test file test/isPrime.test.js for src/isPrime.js with node:test. Cover at least 0, 1, 2, a prime above 10 and a composite number. Run it.",
2257
+ files: {
2258
+ "package.json": pkg,
2259
+ "src/isPrime.js": lines3(
2260
+ "export function isPrime(n) {",
2261
+ " if (n < 2) return false;",
2262
+ " for (let d = 2; d * d <= n; d++) if (n % d === 0) return false;",
2263
+ " return true;",
2264
+ "}"
2265
+ )
2266
+ },
2267
+ check: 'test -f test/isPrime.test.js && node --test && [ "$(grep -c "assert" test/isPrime.test.js)" -ge 4 ]',
2268
+ protect: ["src/isPrime.js"],
2269
+ solution: {
2270
+ "test/isPrime.test.js": `${imports("../src/isPrime.js", "isPrime")}${lines3(
2271
+ 'test("isPrime", () => {',
2272
+ " assert.equal(isPrime(0), false);",
2273
+ " assert.equal(isPrime(1), false);",
2274
+ " assert.equal(isPrime(2), true);",
2275
+ " assert.equal(isPrime(13), true);",
2276
+ " assert.equal(isPrime(15), false);",
2277
+ "});"
2278
+ )}`
2279
+ }
2280
+ },
2281
+ {
2282
+ id: "config-default",
2283
+ title: "Add a default value",
2284
+ prompt: "loadConfig must use port 3000 when the config has no port, but keep a port that is set. Make the tests pass.",
2285
+ files: {
2286
+ "package.json": pkg,
2287
+ "src/config.js": lines3(
2288
+ "export function loadConfig(json) {",
2289
+ " const config = JSON.parse(json);",
2290
+ " return config;",
2291
+ "}"
2292
+ ),
2293
+ "test/config.test.js": `${imports("../src/config.js", "loadConfig")}${lines3(
2294
+ `test("default port", () => assert.equal(loadConfig('{"host":"x"}').port, 3000));`,
2295
+ `test("set port", () => assert.equal(loadConfig('{"port":8080}').port, 8080));`,
2296
+ `test("keeps other keys", () => assert.equal(loadConfig('{"host":"x"}').host, "x"));`
2297
+ )}`
2298
+ },
2299
+ check: "node --test",
2300
+ solution: {
2301
+ "src/config.js": lines3(
2302
+ "export function loadConfig(json) {",
2303
+ " const config = JSON.parse(json);",
2304
+ " return { port: 3000, ...config };",
2305
+ "}"
2306
+ )
2307
+ }
2308
+ },
2309
+ {
2310
+ id: "new-module",
2311
+ title: "Create a new module",
2312
+ prompt: 'Create src/version.js that exports a constant VERSION with the "version" value from package.json (read the file; do not import JSON). The test must pass.',
2313
+ files: {
2314
+ "package.json": '{ "name": "eval", "version": "1.2.3", "type": "module", "private": true }\n',
2315
+ "test/version.test.js": `${imports("../src/version.js", "VERSION")}test("version", () => assert.equal(VERSION, "1.2.3"));
2316
+ `
2317
+ },
2318
+ check: "node --test",
2319
+ solution: {
2320
+ "src/version.js": lines3(
2321
+ 'import { readFileSync } from "node:fs";',
2322
+ 'const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));',
2323
+ "export const VERSION = pkg.version;"
2324
+ )
2325
+ }
2326
+ },
2327
+ {
2328
+ id: "extract-helper",
2329
+ title: "Remove duplicate code",
2330
+ prompt: "signup.js and invite.js have the same e-mail check. Move it into a function isValidEmail(text) exported from src/email.js, and use it in both files. The tests must still pass.",
2331
+ files: {
2332
+ "package.json": pkg,
2333
+ "src/signup.js": lines3(
2334
+ "export function signup(email) {",
2335
+ ' if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(email)) throw new Error("bad email");',
2336
+ " return { email };",
2337
+ "}"
2338
+ ),
2339
+ "src/invite.js": lines3(
2340
+ "export function invite(email) {",
2341
+ ' if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(email)) throw new Error("bad email");',
2342
+ " return { invited: email };",
2343
+ "}"
2344
+ ),
2345
+ "test/email.test.js": `${imports("../src/signup.js", "signup")}${lines3(
2346
+ 'import { invite } from "../src/invite.js";',
2347
+ 'test("signup", () => { assert.equal(signup("a@b.co").email, "a@b.co"); assert.throws(() => signup("nope")); });',
2348
+ 'test("invite", () => { assert.equal(invite("a@b.co").invited, "a@b.co"); assert.throws(() => invite("x@y")); });'
2349
+ )}`
2350
+ },
2351
+ check: [
2352
+ "node --test",
2353
+ `node -e 'import("./src/email.js").then((m) => process.exit(m.isValidEmail("a@b.co") && !m.isValidEmail("x") ? 0 : 1))'`,
2354
+ "grep -q isValidEmail src/signup.js",
2355
+ "grep -q isValidEmail src/invite.js",
2356
+ // The e-mail pattern must appear only once in src/ after the change.
2357
+ '[ "$(cat src/*.js | grep -c "\\[^@")" -le 1 ]'
2358
+ ].join(" && "),
2359
+ solution: {
2360
+ "src/email.js": lines3(
2361
+ "export function isValidEmail(text) {",
2362
+ " return /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(text);",
2363
+ "}"
2364
+ ),
2365
+ "src/signup.js": lines3(
2366
+ 'import { isValidEmail } from "./email.js";',
2367
+ "export function signup(email) {",
2368
+ ' if (!isValidEmail(email)) throw new Error("bad email");',
2369
+ " return { email };",
2370
+ "}"
2371
+ ),
2372
+ "src/invite.js": lines3(
2373
+ 'import { isValidEmail } from "./email.js";',
2374
+ "export function invite(email) {",
2375
+ ' if (!isValidEmail(email)) throw new Error("bad email");',
2376
+ " return { invited: email };",
2377
+ "}"
2378
+ )
2379
+ }
2380
+ }
2381
+ ];
2382
+
2383
+ // src/evals/suites.ts
2384
+ var EVAL_SUITES = {
2385
+ basic: EVAL_TASKS,
2386
+ hard: HARD_TASKS,
2387
+ java: JAVA_TASKS,
2388
+ python: PYTHON_TASKS
2389
+ };
2390
+ var ALL_TASKS = Object.values(EVAL_SUITES).flat();
2391
+ function requiredToolchains(tasks) {
2392
+ return [...new Set(tasks.flatMap((t) => t.requires ?? []))];
2393
+ }
2394
+
2395
+ // src/cli/evalCommand.ts
2396
+ var BATCH_TASK_TIMEOUT_MS = 12 * 60 * 6e4;
2397
+ async function runEvalCommand(options) {
2398
+ if (options.fromGit) return buildFromGit(options);
2399
+ let repoTasks;
2400
+ if (options.list || options.suite === "repo") {
2401
+ const loaded = await repoSuite();
2402
+ if (typeof loaded === "string") {
2403
+ if (options.suite === "repo") {
2404
+ process.stderr.write(`${loaded}
2405
+ `);
2406
+ return 1;
2407
+ }
2408
+ } else repoTasks = repoEvalTasks(loaded);
2409
+ }
2410
+ if (options.list) {
2411
+ const suites = [
2412
+ ...Object.entries(EVAL_SUITES),
2413
+ ...repoTasks === void 0 ? [] : [["repo", repoTasks]]
2414
+ ];
2415
+ for (const [suite2, tasks2] of suites) {
2416
+ const needs = requiredToolchains(tasks2).map((id) => TOOLCHAINS[id].title);
2417
+ process.stdout.write(`${suite2}:${needs.length > 0 ? ` (needs ${needs.join(", ")})` : ""}
2418
+ `);
2419
+ for (const task4 of tasks2) process.stdout.write(` ${task4.id.padEnd(18)} ${task4.title}
2420
+ `);
2421
+ }
2422
+ return 0;
2423
+ }
2424
+ if (options.prepare !== void 0) return prepare(options.prepare);
2425
+ const modelId = options.model;
2426
+ if (!modelId) {
2427
+ process.stderr.write("Set a model with --model <id> or the GARUDA_MODEL variable.\n");
2428
+ return 1;
2429
+ }
2430
+ const models = await loadModelsConfig();
2431
+ if (models.problem !== void 0) {
2432
+ process.stderr.write(`${models.problem}
2433
+ `);
2434
+ return 1;
2435
+ }
2436
+ let resolved;
2437
+ try {
2438
+ resolved = resolveModel(modelId, models.config);
2439
+ } catch (error) {
2440
+ process.stderr.write(`${error.message}
2441
+ `);
2442
+ return 1;
2443
+ }
2444
+ for (const note of resolved.notes) process.stderr.write(`${note}
2445
+ `);
2446
+ const index = options.index ?? DEFAULT_CODE_INDEX_MODE;
2447
+ if (!CODE_INDEX_MODES.includes(index)) {
2448
+ process.stderr.write(
2449
+ `Unknown index mode ${options.index}. Use: ${CODE_INDEX_MODES.join(", ")}.
2450
+ `
2451
+ );
2452
+ return 1;
2453
+ }
2454
+ if (options.executor !== void 0 && !EXECUTOR_NAMES.includes(options.executor)) {
2455
+ process.stderr.write(
2456
+ `Unknown executor ${options.executor}. Use: ${EXECUTOR_NAMES.join(", ")}.
2457
+ `
2458
+ );
2459
+ return 1;
2460
+ }
2461
+ const subagentsMode = options.subagents ?? "off";
2462
+ if (subagentsMode !== "on" && subagentsMode !== "off") {
2463
+ process.stderr.write(`Unknown subagents mode ${options.subagents}. Use: on, off.
2464
+ `);
2465
+ return 1;
2466
+ }
2467
+ const todoMode = options.todo ?? "off";
2468
+ if (todoMode !== "on" && todoMode !== "off") {
2469
+ process.stderr.write(`Unknown todo mode ${options.todo}. Use: on, off.
2470
+ `);
2471
+ return 1;
2472
+ }
2473
+ const formatMode = options.format ?? "off";
2474
+ if (formatMode !== "on" && formatMode !== "off") {
2475
+ process.stderr.write(`Unknown format mode ${options.format}. Use: on, off.
2476
+ `);
2477
+ return 1;
2478
+ }
2479
+ let biome;
2480
+ if (options.format !== void 0) {
2481
+ biome = garudaBiome();
2482
+ if (biome === void 0) {
2483
+ process.stderr.write(
2484
+ "--format needs Biome in Garuda's node_modules (a source checkout after pnpm install).\n"
2485
+ );
2486
+ return 1;
2487
+ }
2488
+ if (options.suite === "java" || options.suite === "python" || options.suite === "all" || options.suite === "repo") {
2489
+ process.stderr.write("--format works with the JS suites: basic and hard.\n");
2490
+ return 1;
2491
+ }
2492
+ }
2493
+ const thinkingMode = options.keepThinking ?? "on";
2494
+ if (thinkingMode !== "on" && thinkingMode !== "off") {
2495
+ process.stderr.write(`Unknown keep-thinking mode ${options.keepThinking}. Use: on, off.
2496
+ `);
2497
+ return 1;
2498
+ }
2499
+ const lspMode = options.lsp ?? "off";
2500
+ if (lspMode !== "on" && lspMode !== "off") {
2501
+ process.stderr.write(`Unknown lsp mode ${options.lsp}. Use: on, off.
2502
+ `);
2503
+ return 1;
2504
+ }
2505
+ let network;
2506
+ if (options.network !== void 0) {
2507
+ network = options.network.flatMap((e) => e.split(",")).filter((e) => e.trim() !== "");
2508
+ const { problems } = expandAllowlist(network);
2509
+ if (problems.length > 0) {
2510
+ process.stderr.write(`--network: ${problems.join(" ")}
2511
+ `);
2512
+ return 1;
2513
+ }
2514
+ if (network.length === 0) network = void 0;
2515
+ }
2516
+ const batchMode = options.batch ?? "off";
2517
+ if (batchMode !== "on" && batchMode !== "off") {
2518
+ process.stderr.write(`Unknown batch mode ${options.batch}. Use: on, off.
2519
+ `);
2520
+ return 1;
2521
+ }
2522
+ const createBatch = resolved.createBatch;
2523
+ if (batchMode === "on" && createBatch === void 0) {
2524
+ process.stderr.write(`--batch on needs an Anthropic model; ${resolved.spec} is not one.
2525
+ `);
2526
+ return 1;
2527
+ }
2528
+ let sub;
2529
+ if (options.subagentModel) {
2530
+ try {
2531
+ sub = resolveModel(options.subagentModel, models.config);
2532
+ } catch (error) {
2533
+ process.stderr.write(`${error.message}
2534
+ `);
2535
+ return 1;
2536
+ }
2537
+ }
2538
+ const { assertModelAllowedByPolicy, loadTeamPolicy } = await import("./policy-EJPLEEXP.js");
2539
+ let team;
2540
+ try {
2541
+ team = await loadTeamPolicy();
2542
+ assertModelAllowedByPolicy(team?.policy, resolved.spec);
2543
+ if (sub !== void 0) assertModelAllowedByPolicy(team?.policy, sub.spec);
2544
+ } catch (error) {
2545
+ process.stderr.write(`Team policy: ${error.message}
2546
+ `);
2547
+ return 1;
2548
+ }
2549
+ const suite = options.suite ?? "basic";
2550
+ const suiteTasks = suite === "all" ? ALL_TASKS : suite === "repo" ? repoTasks : EVAL_SUITES[suite];
2551
+ if (suiteTasks === void 0) {
2552
+ process.stderr.write(
2553
+ `Unknown suite ${suite}. Use: ${[...Object.keys(EVAL_SUITES), "all", "repo"].join(", ")}.
2554
+ `
2555
+ );
2556
+ return 1;
2557
+ }
2558
+ const pool = suite === "repo" ? suiteTasks : ALL_TASKS;
2559
+ const chosen = options.task === void 0 ? suiteTasks : pool.filter((t) => options.task?.includes(t.id));
2560
+ const repeat = Math.max(1, options.repeat ?? 1);
2561
+ const tasks = chosen.flatMap((t) => Array.from({ length: repeat }, () => t));
2562
+ if (chosen.length === 0) {
2563
+ process.stderr.write("No task matches. Use --list to see the task ids.\n");
2564
+ return 1;
2565
+ }
2566
+ const missing = (await checkToolchains(requiredToolchains(chosen))).filter((s) => !s.ok);
2567
+ if (missing.length > 0) {
2568
+ for (const status of missing) {
2569
+ const detail = status.output.split("\n").filter((line) => line.trim() !== "").slice(0, 2).join("\n ");
2570
+ process.stderr.write(
2571
+ `${TOOLCHAINS[status.id].hint}
2572
+ ${detail === "" ? "" : ` ${detail}
2573
+ `}`
2574
+ );
2575
+ }
2576
+ return 1;
2577
+ }
2578
+ const { executor, notice } = createExecutor(options.executor ?? "auto");
2579
+ if (notice !== void 0) process.stderr.write(`${notice}
2580
+ `);
2581
+ if (lspMode === "on") {
2582
+ if (executor.isolation === "none") {
2583
+ process.stderr.write("--lsp on needs the OS sandbox: language servers run only there.\n");
2584
+ return 1;
2585
+ }
2586
+ const { discoverServer, LSP_LANGUAGES } = await import("./servers-6YDGWOZ6.js");
2587
+ const found = LSP_LANGUAGES.map((l) => ({ l, s: discoverServer(l, { root: process.cwd() }) }));
2588
+ if (found.every((f) => f.s === void 0)) {
2589
+ process.stderr.write(
2590
+ "--lsp on: no language server found. Run: garuda lsp install typescript\n"
2591
+ );
2592
+ return 1;
2593
+ }
2594
+ for (const { l, s } of found) {
2595
+ process.stderr.write(
2596
+ `LSP ${l}: ${s === void 0 ? "none" : `${s.spec.name} (${s.path})`}
2597
+ `
2598
+ );
2599
+ }
2600
+ }
2601
+ const where = executor.isolation === "none" ? "commands run on this machine with no sandbox" : `commands and the checks run in the ${executor.name} sandbox`;
2602
+ const parallel = Math.max(
2603
+ 1,
2604
+ options.parallel ?? (batchMode === "on" ? Math.min(tasks.length, 20) : 1)
2605
+ );
2606
+ const outDir = join5(process.cwd(), ".garuda", "evals", newSessionId());
2607
+ mkdirSync3(outDir, { recursive: true });
2608
+ process.stderr.write(
2609
+ `Running ${chosen.length} task(s)${repeat > 1 ? ` \xD7 ${repeat}` : ""} (suite ${options.task === void 0 ? suite : "custom"}, code index ${index}, subagents ${subagentsMode}${sub === void 0 ? "" : ` on ${sub.spec}`}, todo ${todoMode}, lsp ${lspMode}, keep-thinking ${thinkingMode}, format ${formatMode}, network ${network === void 0 ? "off" : network.join(",")}, batch ${batchMode}${parallel > 1 ? `, ${parallel} at a time` : ""}) with ${modelId}. Garuda approves every call except its deny rules;
2610
+ ${where}, in scratch folders.
2611
+
2612
+ `
2613
+ );
2614
+ if (batchMode === "on") {
2615
+ process.stderr.write(
2616
+ "Batch API: each model call waits for its batch (minutes, sometimes more) at half the token price.\n\n"
2617
+ );
2618
+ }
2619
+ const started = Date.now();
2620
+ const results = await runEvals(
2621
+ tasks,
2622
+ {
2623
+ modelId: resolved.spec,
2624
+ ...team === void 0 ? {} : { policy: team.policy },
2625
+ model: () => batchMode === "on" && createBatch !== void 0 ? createBatch() : resolved.create(),
2626
+ // Batch responses carry their own price factor (half): the price stays the model's.
2627
+ modelInfo: resolved.info,
2628
+ parallel,
2629
+ ...batchMode === "on" ? { taskTimeoutMs: BATCH_TASK_TIMEOUT_MS } : {},
2630
+ ...resolved.maxTokens === void 0 ? {} : { maxTokens: resolved.maxTokens },
2631
+ outDir,
2632
+ ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
2633
+ codeIndex: index,
2634
+ subagents: subagentsMode === "on",
2635
+ todo: todoMode === "on",
2636
+ lsp: lspMode === "on",
2637
+ keepThinking: thinkingMode === "on",
2638
+ ...network === void 0 ? {} : { network },
2639
+ ...biome === void 0 ? {} : { format: { on: formatMode === "on", biome } },
2640
+ ...sub === void 0 ? {} : { subagentModel: { spec: sub.spec, model: () => sub.create(), info: sub.info } },
2641
+ executor: executor.name === "host" ? "host" : "os",
2642
+ ...options.keep ? { keep: true } : {},
2643
+ onEvent: (taskId, event) => {
2644
+ if (event.type === "tool_call") process.stderr.write(` [${taskId}] ${event.call.name}
2645
+ `);
2646
+ if (event.type === "notice") process.stderr.write(` [${taskId}] ${event.text}
2647
+ `);
2648
+ if (event.type === "model_retry")
2649
+ process.stderr.write(
2650
+ ` [${taskId}] retry ${event.attempt}/${event.maxRetries}: ${event.reason}
2651
+ `
2652
+ );
2653
+ }
2654
+ },
2655
+ (r) => process.stderr.write(
2656
+ `${r.passed ? "\u2713" : "\u2717"} ${r.id}: ${r.passed ? "pass" : "fail"} (${r.steps} steps)${r.reason ? `
2657
+ ${r.reason.split("\n").slice(0, 3).join("\n ")}` : ""}
2658
+ `
2659
+ )
2660
+ );
2661
+ const wallMs = Date.now() - started;
2662
+ const report2 = `${formatReport(results)}
2663
+ Wall time: ${Math.round(wallMs / 1e3)} s${parallel > 1 ? ` (${parallel} tasks at a time)` : ""}.`;
2664
+ writeFileSync(
2665
+ join5(outDir, "report.json"),
2666
+ `${JSON.stringify({ model: modelId, suite, repeat, codeIndex: index, subagents: subagentsMode, subagentModel: sub?.spec, todo: todoMode, lsp: lspMode, keepThinking: thinkingMode, format: formatMode, network: network ?? "off", batch: batchMode, parallel, wallMs, executor: executor.name, results }, null, 2)}
2667
+ `
2668
+ );
2669
+ process.stdout.write(`
2670
+ ${report2}
2671
+
2672
+ Session files and report.json: ${outDir}
2673
+ `);
2674
+ return results.every((r) => r.passed) ? 0 : 2;
2675
+ }
2676
+ async function repoSuite() {
2677
+ const root = await gitRoot(process.cwd());
2678
+ if (root === void 0) return "-s repo needs a git repository: run it in your project.";
2679
+ let suite;
2680
+ try {
2681
+ suite = await loadRepoSuite(root);
2682
+ } catch (error) {
2683
+ return error.message;
2684
+ }
2685
+ if (suite === void 0) {
2686
+ return `No repo suite in ${root}. Build it first: garuda eval --from-git`;
2687
+ }
2688
+ if (suite.tasks.length === 0) {
2689
+ return `The repo suite has no tasks. Build it again with more commits: garuda eval --from-git --commits 500`;
2690
+ }
2691
+ return { ...suite, root };
2692
+ }
2693
+ async function gitRoot(cwd) {
2694
+ const r = await git(new HostExecutor(), cwd, ["rev-parse", "--show-toplevel"], { check: false });
2695
+ return r.exitCode === 0 ? realpathSync3(r.stdout.trim()) : void 0;
2696
+ }
2697
+ async function buildFromGit(options) {
2698
+ const root = await gitRoot(process.cwd());
2699
+ if (root === void 0) {
2700
+ process.stderr.write("--from-git needs a git repository: run it in your project.\n");
2701
+ return 1;
2702
+ }
2703
+ const testCommand = options.testCommand ?? detectTestCommand(root, detectProfiles(root));
2704
+ if (testCommand === void 0) {
2705
+ process.stderr.write(
2706
+ 'No test command found. Give one: garuda eval --from-git --test-command "npm test"\n'
2707
+ );
2708
+ return 1;
2709
+ }
2710
+ for (const [name, value] of [
2711
+ ["--commits", options.commits],
2712
+ ["--max-tasks", options.maxTasks]
2713
+ ]) {
2714
+ if (value !== void 0 && !(Number.isInteger(value) && value > 0)) {
2715
+ process.stderr.write(`${name} must be a whole number above 0.
2716
+ `);
2717
+ return 1;
2718
+ }
2719
+ }
2720
+ process.stderr.write(
2721
+ `Building the repo suite from ${options.commits ?? DEFAULT_COMMITS} recent commits${options.since === void 0 ? "" : ` since ${options.since}`} in ${root}.
2722
+ Each candidate runs its own test files with \`${testCommand}\` twice on this machine (no sandbox), in a git worktree; your checkout does not change.
2723
+
2724
+ `
2725
+ );
2726
+ const { suite, checked, skipped, failure } = await buildRepoSuite(new HostExecutor(), root, {
2727
+ testCommand,
2728
+ ...options.commits === void 0 ? {} : { commits: options.commits },
2729
+ ...options.since === void 0 ? {} : { since: options.since },
2730
+ ...options.maxTasks === void 0 ? {} : { maxTasks: options.maxTasks },
2731
+ onProgress: (line) => process.stderr.write(` ${line}
2732
+ `)
2733
+ });
2734
+ const file = await saveRepoSuite(suite);
2735
+ const reasons = Object.entries(skipped).sort((a, b) => b[1] - a[1]).map(([why, n]) => ` ${String(n).padStart(4)} ${why}`);
2736
+ process.stdout.write(
2737
+ `${[
2738
+ `
2739
+ ${suite.tasks.length} task(s) kept; ${checked} candidate(s) checked with the tests.`,
2740
+ ...reasons.length === 0 ? [] : ["Skipped:", ...reasons],
2741
+ ...failure === void 0 ? [] : [
2742
+ `The first failure at a commit (${failure.sha.slice(0, 7)}: \`${failure.command}\`):`,
2743
+ ...failure.tail.split("\n").map((line) => ` ${line}`),
2744
+ 'If every commit fails like this, the tests do not run in a worktree on this machine; give a working command with --test-command ("{files}" takes the test files).'
2745
+ ],
2746
+ `Suite: ${file}`,
2747
+ ...suite.tasks.length === 0 ? ["No task: try more commits (--commits 500) or an older --since."] : ["Run it: garuda eval -s repo -m <model> (add --list to see the tasks)"]
2748
+ ].join("\n")}
2749
+ `
2750
+ );
2751
+ return suite.tasks.length === 0 ? 2 : 0;
2752
+ }
2753
+ async function prepare(name) {
2754
+ const id = toolchainId(name);
2755
+ if (id === void 0) {
2756
+ process.stderr.write(`Unknown toolchain ${name}. Use: java, python.
2757
+ `);
2758
+ return 1;
2759
+ }
2760
+ const toolchain = TOOLCHAINS[id];
2761
+ if (toolchain.prepare !== void 0) {
2762
+ process.stderr.write(
2763
+ `Preparing ${toolchain.title}: running \`${toolchain.prepare}\` in a scratch project (downloads with network)\u2026
2764
+ `
2765
+ );
2766
+ const r = await prepareToolchain(id);
2767
+ if (!r.ok) {
2768
+ process.stderr.write(`It failed:
2769
+ ${r.output}
2770
+ `);
2771
+ return 1;
2772
+ }
2773
+ }
2774
+ const [status] = await checkToolchains([id]);
2775
+ if (status?.ok) {
2776
+ process.stderr.write(`${toolchain.title}: ready.
2777
+ `);
2778
+ return 0;
2779
+ }
2780
+ process.stderr.write(`${toolchain.hint}
2781
+ ${status?.output ?? ""}
2782
+ `);
2783
+ return 1;
2784
+ }
2785
+ function garudaBiome() {
2786
+ try {
2787
+ const pkg2 = createRequire(import.meta.url).resolve("@biomejs/biome/package.json");
2788
+ const bin = join5(dirname3(pkg2), "bin", "biome");
2789
+ return existsSync2(bin) ? bin : void 0;
2790
+ } catch {
2791
+ return void 0;
2792
+ }
2793
+ }
2794
+ export {
2795
+ BATCH_TASK_TIMEOUT_MS,
2796
+ runEvalCommand
2797
+ };
2798
+ //# sourceMappingURL=evalCommand-3SSFMZQD.js.map