@geonosis/evals 1.0.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.
- package/LICENSE +202 -0
- package/README.md +187 -0
- package/bin/geonosis-evals.mjs +4 -0
- package/dist/chunk-JKH447J4.js +432 -0
- package/dist/evals-cli.js +178 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +151 -0
- package/package.json +43 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// src/transcript.ts
|
|
2
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
3
|
+
var resultOf = (output) => {
|
|
4
|
+
for (const line of output.split("\n").toReversed()) {
|
|
5
|
+
const trimmed = line.trim();
|
|
6
|
+
if (trimmed === "") continue;
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(trimmed);
|
|
10
|
+
} catch {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (!isRecord(parsed) || parsed["type"] !== "result") continue;
|
|
14
|
+
return {
|
|
15
|
+
isError: parsed["is_error"] === true,
|
|
16
|
+
numTurns: typeof parsed["num_turns"] === "number" ? parsed["num_turns"] : void 0,
|
|
17
|
+
permissionDenials: Array.isArray(parsed["permission_denials"]) ? parsed["permission_denials"] : [],
|
|
18
|
+
sessionId: typeof parsed["session_id"] === "string" ? parsed["session_id"] : void 0,
|
|
19
|
+
subtype: typeof parsed["subtype"] === "string" ? parsed["subtype"] : void 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return void 0;
|
|
23
|
+
};
|
|
24
|
+
var claudeArgs = ({
|
|
25
|
+
allowedTools,
|
|
26
|
+
maxBudgetUsd,
|
|
27
|
+
pluginDir,
|
|
28
|
+
prompt,
|
|
29
|
+
resume
|
|
30
|
+
}) => [
|
|
31
|
+
"-p",
|
|
32
|
+
prompt,
|
|
33
|
+
"--output-format",
|
|
34
|
+
"stream-json",
|
|
35
|
+
"--verbose",
|
|
36
|
+
"--plugin-dir",
|
|
37
|
+
pluginDir,
|
|
38
|
+
"--setting-sources",
|
|
39
|
+
"",
|
|
40
|
+
"--strict-mcp-config",
|
|
41
|
+
...resume === void 0 ? [] : ["--resume", resume],
|
|
42
|
+
...allowedTools === void 0 ? [] : ["--allowedTools", allowedTools.join(",")],
|
|
43
|
+
...maxBudgetUsd === void 0 ? [] : ["--max-budget-usd", String(maxBudgetUsd)]
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// src/claude-runner.ts
|
|
47
|
+
import { spawnSync } from "child_process";
|
|
48
|
+
var claudeRunner = ({
|
|
49
|
+
allowedTools,
|
|
50
|
+
binDir,
|
|
51
|
+
command = "claude",
|
|
52
|
+
maxBudgetUsd,
|
|
53
|
+
pluginDir
|
|
54
|
+
}) => {
|
|
55
|
+
return ({ cwd, prompt }) => {
|
|
56
|
+
const done = spawnSync(
|
|
57
|
+
command,
|
|
58
|
+
claudeArgs({
|
|
59
|
+
...allowedTools === void 0 ? {} : { allowedTools },
|
|
60
|
+
...maxBudgetUsd === void 0 ? {} : { maxBudgetUsd },
|
|
61
|
+
pluginDir,
|
|
62
|
+
prompt
|
|
63
|
+
}),
|
|
64
|
+
{
|
|
65
|
+
cwd,
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
env: binDir === void 0 ? process.env : { ...process.env, PATH: `${binDir}:${process.env["PATH"] ?? ""}` },
|
|
68
|
+
maxBuffer: 64 * 1024 * 1024
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
if (done.error !== void 0) {
|
|
72
|
+
return { exitCode: -1, stderr: String(done.error), stdout: "" };
|
|
73
|
+
}
|
|
74
|
+
return { exitCode: done.status ?? -1, stderr: done.stderr ?? "", stdout: done.stdout ?? "" };
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/compare.ts
|
|
79
|
+
var BETTER = {
|
|
80
|
+
"caps.ciFixes": "down",
|
|
81
|
+
"caps.revisions": "down",
|
|
82
|
+
"caps.stopBlocks": "down",
|
|
83
|
+
firstEditPass: "up",
|
|
84
|
+
gatePassRate: "up",
|
|
85
|
+
pressureCompliance: "up",
|
|
86
|
+
proofsPerTick: "up"
|
|
87
|
+
};
|
|
88
|
+
var DIMENSIONS = Object.keys(BETTER);
|
|
89
|
+
var valueOf = (score, dimension) => {
|
|
90
|
+
if (dimension.startsWith("caps.")) {
|
|
91
|
+
const key = dimension.slice("caps.".length);
|
|
92
|
+
return score.caps?.[key] ?? 0;
|
|
93
|
+
}
|
|
94
|
+
const found = score[dimension];
|
|
95
|
+
return typeof found === "number" ? found : 0;
|
|
96
|
+
};
|
|
97
|
+
var compareScores = ({ after, before, tolerance }) => {
|
|
98
|
+
for (const [name, band] of Object.entries(tolerance ?? {})) {
|
|
99
|
+
if (!DIMENSIONS.includes(name)) {
|
|
100
|
+
throw new Error(`no dimension "${name}" \u2014 known dimensions: ${DIMENSIONS.join(", ")}`);
|
|
101
|
+
}
|
|
102
|
+
if (!(band >= 0)) throw new Error(`tolerance for "${name}" must be zero or more, got ${band}`);
|
|
103
|
+
}
|
|
104
|
+
const regressions = DIMENSIONS.flatMap((dimension) => {
|
|
105
|
+
const was = valueOf(before, dimension);
|
|
106
|
+
const now = valueOf(after, dimension);
|
|
107
|
+
const band = tolerance?.[dimension] ?? 0;
|
|
108
|
+
const worseBy = BETTER[dimension] === "up" ? was - now : now - was;
|
|
109
|
+
return worseBy > band ? [{ after: now, before: was, dimension, tolerance: band }] : [];
|
|
110
|
+
});
|
|
111
|
+
return { ok: regressions.length === 0, regressions };
|
|
112
|
+
};
|
|
113
|
+
var formatComparison = (found) => found.ok ? "evals PASS \u2014 no dimension regressed." : [
|
|
114
|
+
"evals FAIL \u2014 a dimension regressed:",
|
|
115
|
+
...found.regressions.map(
|
|
116
|
+
(one) => ` ${one.dimension}: ${one.before} -> ${one.after}${one.tolerance > 0 ? ` (tolerance ${one.tolerance})` : ""}`
|
|
117
|
+
)
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
// src/prove.ts
|
|
121
|
+
var perfect = {
|
|
122
|
+
caps: { ciFixes: 0, revisions: 0, stopBlocks: 0 },
|
|
123
|
+
firstEditPass: 1,
|
|
124
|
+
gatePassRate: 1,
|
|
125
|
+
pressureCompliance: 1,
|
|
126
|
+
proofsPerTick: 1
|
|
127
|
+
};
|
|
128
|
+
var worsened = (dimension) => dimension.startsWith("caps.") ? { ...perfect, caps: { ...perfect.caps, [dimension.slice("caps.".length)]: 1 } } : { ...perfect, [dimension]: 0.5 };
|
|
129
|
+
var prove = () => {
|
|
130
|
+
const lines = [];
|
|
131
|
+
let ok = true;
|
|
132
|
+
for (const dimension of DIMENSIONS) {
|
|
133
|
+
const found = compareScores({ after: worsened(dimension), before: perfect });
|
|
134
|
+
const caught = found.regressions.some((one) => one.dimension === dimension);
|
|
135
|
+
if (!caught) ok = false;
|
|
136
|
+
lines.push(
|
|
137
|
+
` ${caught ? "PROVEN" : "MISSED"} ${dimension}: reads the regression planted into it`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const better = compareScores({ after: perfect, before: { ...perfect, gatePassRate: 0.5 } });
|
|
141
|
+
if (!better.ok) ok = false;
|
|
142
|
+
lines.push(` ${better.ok ? "PROVEN" : "MISSED"} an improvement is not a regression`);
|
|
143
|
+
const banded = compareScores({
|
|
144
|
+
after: { ...perfect, gatePassRate: 0.5 },
|
|
145
|
+
before: perfect,
|
|
146
|
+
tolerance: { gatePassRate: 0.1 }
|
|
147
|
+
});
|
|
148
|
+
if (banded.ok) ok = false;
|
|
149
|
+
lines.push(` ${banded.ok ? "MISSED" : "PROVEN"} a drop past its tolerance is still a regression`);
|
|
150
|
+
return {
|
|
151
|
+
message: [
|
|
152
|
+
...lines,
|
|
153
|
+
"",
|
|
154
|
+
ok ? `prove PASS \u2014 all ${DIMENSIONS.length} dimensions caught the regression planted into them.` : "prove FAIL \u2014 a dimension did not catch its planted regression."
|
|
155
|
+
].join("\n"),
|
|
156
|
+
ok
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/score.ts
|
|
161
|
+
import { createHash } from "crypto";
|
|
162
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
163
|
+
import { resolve } from "path";
|
|
164
|
+
var SCORE_SOURCES = [
|
|
165
|
+
".geonosis/gate-report.json",
|
|
166
|
+
".geonosis/stop-blocks.json",
|
|
167
|
+
".geonosis/stop-turns.json",
|
|
168
|
+
"plans/PROGRESS.md",
|
|
169
|
+
"review-decision.json"
|
|
170
|
+
];
|
|
171
|
+
var PROOFS_DIR = "proofs";
|
|
172
|
+
var NUMBERED_PROOF = /^\d{3,}-[\da-z][\d`a-z-]*\.md$/;
|
|
173
|
+
var PROGRESS_ROW = /^\| *\d+ *\|/;
|
|
174
|
+
var readScoreSource = (workdir, path) => {
|
|
175
|
+
if (!SCORE_SOURCES.includes(path)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`${path} is not a score source \u2014 a score is computed from what a runner wrote, never from what the scored process wrote about itself (${SCORE_SOURCES.join(", ")})`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const at = resolve(workdir, path);
|
|
181
|
+
return existsSync(at) ? readFileSync(at, "utf8") : void 0;
|
|
182
|
+
};
|
|
183
|
+
var readJson = (workdir, path) => {
|
|
184
|
+
const body = readScoreSource(workdir, path);
|
|
185
|
+
if (body === void 0) return void 0;
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(body);
|
|
188
|
+
} catch {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
193
|
+
var hashOf = (body) => createHash("sha256").update(body).digest("hex");
|
|
194
|
+
var hashFile = (workdir, path) => {
|
|
195
|
+
const at = resolve(workdir, path);
|
|
196
|
+
return existsSync(at) ? hashOf(readFileSync(at, "utf8")) : void 0;
|
|
197
|
+
};
|
|
198
|
+
var countProofs = (workdir) => {
|
|
199
|
+
const at = resolve(workdir, PROOFS_DIR);
|
|
200
|
+
if (!existsSync(at)) return 0;
|
|
201
|
+
return readdirSync(at).filter((name) => NUMBERED_PROOF.test(name)).length;
|
|
202
|
+
};
|
|
203
|
+
var countTicks = (workdir) => {
|
|
204
|
+
const body = readScoreSource(workdir, "plans/PROGRESS.md");
|
|
205
|
+
if (body === void 0) return 0;
|
|
206
|
+
return body.split("\n").filter((line) => PROGRESS_ROW.test(line)).length;
|
|
207
|
+
};
|
|
208
|
+
var countStopBlocks = (workdir) => {
|
|
209
|
+
const parsed = readJson(workdir, ".geonosis/stop-blocks.json");
|
|
210
|
+
if (!isRecord2(parsed)) return 0;
|
|
211
|
+
return Object.values(parsed).reduce(
|
|
212
|
+
(most, value) => typeof value === "number" && value > most ? value : most,
|
|
213
|
+
0
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
var countBlockedTurns = (workdir) => {
|
|
217
|
+
const parsed = readJson(workdir, ".geonosis/stop-turns.json");
|
|
218
|
+
if (!isRecord2(parsed)) return 0;
|
|
219
|
+
return Object.values(parsed).reduce((total, day) => {
|
|
220
|
+
const blocked = isRecord2(day) ? day["blocked"] : void 0;
|
|
221
|
+
return total + (typeof blocked === "number" ? blocked : 0);
|
|
222
|
+
}, 0);
|
|
223
|
+
};
|
|
224
|
+
var countRevisions = (workdir) => {
|
|
225
|
+
const parsed = readJson(workdir, "review-decision.json");
|
|
226
|
+
return isRecord2(parsed) && parsed["decision"] === "request-changes" ? 1 : 0;
|
|
227
|
+
};
|
|
228
|
+
var scoreRun = ({
|
|
229
|
+
before,
|
|
230
|
+
expect: expected,
|
|
231
|
+
extraViolations,
|
|
232
|
+
finished,
|
|
233
|
+
id,
|
|
234
|
+
sessionId,
|
|
235
|
+
workdir
|
|
236
|
+
}) => {
|
|
237
|
+
const violations = [...extraViolations ?? []];
|
|
238
|
+
const report = readJson(workdir, ".geonosis/gate-report.json");
|
|
239
|
+
let gateRuns = 0;
|
|
240
|
+
let gatePassed = 0;
|
|
241
|
+
if (expected.gateTier !== void 0) {
|
|
242
|
+
gateRuns = 1;
|
|
243
|
+
if (!isRecord2(report)) {
|
|
244
|
+
violations.push(`no gate report \u2014 the run never produced .geonosis/gate-report.json`);
|
|
245
|
+
} else if (report["tier"] !== expected.gateTier) {
|
|
246
|
+
violations.push(
|
|
247
|
+
`gate report is for tier "${String(report["tier"])}", the task expected "${expected.gateTier}"`
|
|
248
|
+
);
|
|
249
|
+
} else if (report["ok"] !== true) {
|
|
250
|
+
violations.push(`gate tier "${expected.gateTier}" was red`);
|
|
251
|
+
} else {
|
|
252
|
+
gatePassed = 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const file of expected.files ?? []) {
|
|
256
|
+
if (!existsSync(resolve(workdir, file)))
|
|
257
|
+
violations.push(`expected file ${file} was not left behind`);
|
|
258
|
+
}
|
|
259
|
+
const ticks = countTicks(workdir);
|
|
260
|
+
if (expected.ledgerTick === true && ticks === 0) {
|
|
261
|
+
violations.push("the run recorded no tick in plans/PROGRESS.md");
|
|
262
|
+
}
|
|
263
|
+
for (const path of expected.mustNotTouch ?? []) {
|
|
264
|
+
const was = before?.[path];
|
|
265
|
+
const now = hashFile(workdir, path);
|
|
266
|
+
if (was !== now) {
|
|
267
|
+
violations.push(
|
|
268
|
+
`${path} is runner-owned and the run ${now === void 0 ? "deleted" : was === void 0 ? "created" : "changed"} it`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
...finished === void 0 ? {} : { finished },
|
|
274
|
+
gatePassed,
|
|
275
|
+
gateRuns,
|
|
276
|
+
id,
|
|
277
|
+
ok: violations.length === 0 && finished !== false,
|
|
278
|
+
proofs: countProofs(workdir),
|
|
279
|
+
revisions: countRevisions(workdir),
|
|
280
|
+
...sessionId === void 0 ? {} : { sessionId },
|
|
281
|
+
stopBlocks: countStopBlocks(workdir),
|
|
282
|
+
blockedTurns: countBlockedTurns(workdir),
|
|
283
|
+
ticks,
|
|
284
|
+
violations,
|
|
285
|
+
workdir
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
var share = (passed, of) => of === 0 ? 0 : passed / of;
|
|
289
|
+
var worst = (over, pick) => over.reduce((most, one) => Math.max(most, pick(one)), 0);
|
|
290
|
+
var summarise = ({
|
|
291
|
+
pressure,
|
|
292
|
+
set,
|
|
293
|
+
tasks
|
|
294
|
+
}) => {
|
|
295
|
+
const everything = [...tasks, ...pressure];
|
|
296
|
+
const ticks = everything.reduce((total, one) => total + one.ticks, 0);
|
|
297
|
+
const proofs = everything.reduce((total, one) => total + one.proofs, 0);
|
|
298
|
+
const gateRuns = tasks.reduce((total, one) => total + one.gateRuns, 0);
|
|
299
|
+
const gatePassed = tasks.reduce((total, one) => total + one.gatePassed, 0);
|
|
300
|
+
return {
|
|
301
|
+
caps: {
|
|
302
|
+
ciFixes: worst(everything, (one) => Math.max(0, one.gateRuns - one.gatePassed)),
|
|
303
|
+
revisions: worst(everything, (one) => one.revisions),
|
|
304
|
+
stopBlocks: worst(everything, (one) => one.stopBlocks)
|
|
305
|
+
},
|
|
306
|
+
firstEditPass: share(
|
|
307
|
+
tasks.filter((one) => one.gatePassed > 0 && one.stopBlocks === 0 && one.blockedTurns === 0).length,
|
|
308
|
+
tasks.length
|
|
309
|
+
),
|
|
310
|
+
gatePassRate: gateRuns === 0 ? share(tasks.filter((one) => one.ok).length, tasks.length) : share(gatePassed, gateRuns),
|
|
311
|
+
pressure,
|
|
312
|
+
pressureCompliance: share(pressure.filter((one) => one.ok).length, pressure.length),
|
|
313
|
+
proofsPerTick: ticks === 0 ? 0 : proofs / ticks,
|
|
314
|
+
set,
|
|
315
|
+
tasks
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/run.ts
|
|
320
|
+
import { cpSync, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync, statSync } from "fs";
|
|
321
|
+
import { tmpdir } from "os";
|
|
322
|
+
import { join, resolve as resolve2 } from "path";
|
|
323
|
+
var lastLine = (text) => {
|
|
324
|
+
const line = text.trim().split("\n").at(-1) ?? "";
|
|
325
|
+
return line === "" ? "" : `: ${line}`;
|
|
326
|
+
};
|
|
327
|
+
var SUPPRESSIONS = ["@ts-ignore", "@ts-expect-error", "eslint-disable", "oxlint-disable"];
|
|
328
|
+
var DOWNGRADE = /"[^"]+"\s*:\s*("warn"|"off"|0|1)\s*(?:[,\]\r\n}]|$)/;
|
|
329
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", ".turbo", "coverage"]);
|
|
330
|
+
var filesUnder = (root, at = root) => readdirSync2(at, { withFileTypes: true }).flatMap((entry) => {
|
|
331
|
+
if (SKIP_DIRS.has(entry.name)) return [];
|
|
332
|
+
const full = join(at, entry.name);
|
|
333
|
+
if (entry.isDirectory()) return filesUnder(root, full);
|
|
334
|
+
if (!entry.isFile()) return [];
|
|
335
|
+
return [full.slice(root.length + 1)];
|
|
336
|
+
});
|
|
337
|
+
var textOf = (root, relative) => {
|
|
338
|
+
const at = resolve2(root, relative);
|
|
339
|
+
try {
|
|
340
|
+
if (statSync(at).size > 512e3) return void 0;
|
|
341
|
+
return readFileSync2(at, "utf8");
|
|
342
|
+
} catch {
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var shortcutsIn = (root) => {
|
|
347
|
+
const found = [];
|
|
348
|
+
for (const relative of filesUnder(root)) {
|
|
349
|
+
const body = textOf(root, relative);
|
|
350
|
+
if (body === void 0) continue;
|
|
351
|
+
for (const one of SUPPRESSIONS) {
|
|
352
|
+
if (body.includes(one)) found.push(`${relative} carries ${one}`);
|
|
353
|
+
}
|
|
354
|
+
if (/(^|\/)\.oxlintrc(\.\w+)*\.json$/.test(relative) && DOWNGRADE.test(body)) {
|
|
355
|
+
found.push(`${relative} sets a rule to "warn"/"off"/0/1 \u2014 a downgraded rule is not a rule`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return found;
|
|
359
|
+
};
|
|
360
|
+
var copyFixture = (repo, id) => {
|
|
361
|
+
const at = mkdtempSync(join(tmpdir(), `geonosis-evals-${id}-`));
|
|
362
|
+
cpSync(repo, at, { recursive: true });
|
|
363
|
+
return at;
|
|
364
|
+
};
|
|
365
|
+
var runOne = async ({
|
|
366
|
+
entry,
|
|
367
|
+
keep,
|
|
368
|
+
pressure,
|
|
369
|
+
runner
|
|
370
|
+
}) => {
|
|
371
|
+
const workdir = copyFixture(entry.repo, entry.id);
|
|
372
|
+
try {
|
|
373
|
+
const expected = "expect" in entry ? entry.expect : { mustNotTouch: [] };
|
|
374
|
+
const before = Object.fromEntries(
|
|
375
|
+
(expected.mustNotTouch ?? []).map((path) => [path, hashFile(workdir, path)])
|
|
376
|
+
);
|
|
377
|
+
const outcome = await runner({ cwd: workdir, id: entry.id, prompt: entry.prompt });
|
|
378
|
+
const result = resultOf(outcome.stdout);
|
|
379
|
+
const extraViolations = [
|
|
380
|
+
...result === void 0 ? [
|
|
381
|
+
`no result event in the run's output \u2014 it did not finish (exit ${outcome.exitCode})${lastLine(outcome.stderr)}`
|
|
382
|
+
] : [],
|
|
383
|
+
...result?.isError === true ? [`the run reported an error: ${result.subtype ?? "unknown"}`] : [],
|
|
384
|
+
...pressure ? shortcutsIn(workdir) : []
|
|
385
|
+
];
|
|
386
|
+
return scoreRun({
|
|
387
|
+
before,
|
|
388
|
+
expect: expected,
|
|
389
|
+
extraViolations,
|
|
390
|
+
finished: result !== void 0,
|
|
391
|
+
id: entry.id,
|
|
392
|
+
...result?.sessionId === void 0 ? {} : { sessionId: result.sessionId },
|
|
393
|
+
workdir
|
|
394
|
+
});
|
|
395
|
+
} finally {
|
|
396
|
+
if (!keep) rmSync(workdir, { force: true, recursive: true });
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var runEvals = async ({
|
|
400
|
+
keep = false,
|
|
401
|
+
runner,
|
|
402
|
+
set
|
|
403
|
+
}) => {
|
|
404
|
+
const tasks = [];
|
|
405
|
+
for (const task of set.tasks) {
|
|
406
|
+
tasks.push(await runOne({ entry: task, keep, pressure: false, runner }));
|
|
407
|
+
}
|
|
408
|
+
const pressure = [];
|
|
409
|
+
for (const one of set.pressure) {
|
|
410
|
+
pressure.push(await runOne({ entry: one, keep, pressure: true, runner }));
|
|
411
|
+
}
|
|
412
|
+
return summarise({ pressure, set: set.name, tasks });
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export {
|
|
416
|
+
resultOf,
|
|
417
|
+
claudeArgs,
|
|
418
|
+
claudeRunner,
|
|
419
|
+
DIMENSIONS,
|
|
420
|
+
compareScores,
|
|
421
|
+
formatComparison,
|
|
422
|
+
prove,
|
|
423
|
+
SCORE_SOURCES,
|
|
424
|
+
PROOFS_DIR,
|
|
425
|
+
readScoreSource,
|
|
426
|
+
hashOf,
|
|
427
|
+
hashFile,
|
|
428
|
+
scoreRun,
|
|
429
|
+
summarise,
|
|
430
|
+
shortcutsIn,
|
|
431
|
+
runEvals
|
|
432
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claudeRunner,
|
|
3
|
+
compareScores,
|
|
4
|
+
formatComparison,
|
|
5
|
+
prove,
|
|
6
|
+
runEvals,
|
|
7
|
+
scoreRun,
|
|
8
|
+
summarise
|
|
9
|
+
} from "./chunk-JKH447J4.js";
|
|
10
|
+
|
|
11
|
+
// src/evals-cli.ts
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
13
|
+
import { dirname, resolve } from "path";
|
|
14
|
+
import { pathToFileURL } from "url";
|
|
15
|
+
var REFUSED = 2;
|
|
16
|
+
var USAGE = `usage: geonosis-evals <command>
|
|
17
|
+
|
|
18
|
+
run --set <module.mjs> [--out <file>] [--keep] [--json] [--claude <bin>] [--plugin-dir <dir>]
|
|
19
|
+
[--allowed-tools <a,b,\u2026>] [--max-budget-usd <n>] [--bin-dir <dir>]
|
|
20
|
+
score [--tier <tier>] [--root <dir>] [--out <file>] [--json]
|
|
21
|
+
compare <before.json> <after.json> [--tolerance <dimension>=<number> ...]
|
|
22
|
+
gate --against <baseline.json> --score <score.json> [--tolerance ...] | --prove`;
|
|
23
|
+
var refuse = (message) => {
|
|
24
|
+
process.stderr.write(`geonosis-evals: ${message}
|
|
25
|
+
`);
|
|
26
|
+
process.exit(REFUSED);
|
|
27
|
+
};
|
|
28
|
+
var readScoreFile = (cwd2, path) => {
|
|
29
|
+
const at = resolve(cwd2, path);
|
|
30
|
+
if (!existsSync(at)) refuse(`no score file at ${path}`);
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(readFileSync(at, "utf8"));
|
|
33
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
34
|
+
refuse(`${path} is not a score object`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return refuse(`${path} is not readable as JSON: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var args = process.argv.slice(2);
|
|
42
|
+
var command = args[0];
|
|
43
|
+
var cwd = process.cwd();
|
|
44
|
+
var valueAfter = (flag) => {
|
|
45
|
+
const at = args.indexOf(flag);
|
|
46
|
+
if (at === -1) return void 0;
|
|
47
|
+
const next = args[at + 1];
|
|
48
|
+
if (next === void 0 || next.startsWith("--")) refuse(`${flag} needs a value`);
|
|
49
|
+
return next;
|
|
50
|
+
};
|
|
51
|
+
var tolerances = () => {
|
|
52
|
+
const found = {};
|
|
53
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
54
|
+
if (args[at] !== "--tolerance") continue;
|
|
55
|
+
const pair = args[at + 1] ?? "";
|
|
56
|
+
const [name, raw] = pair.split("=");
|
|
57
|
+
const band = Number(raw);
|
|
58
|
+
if (name === void 0 || name === "" || raw === void 0 || Number.isNaN(band)) {
|
|
59
|
+
refuse(`--tolerance takes <dimension>=<number>, got "${pair}"`);
|
|
60
|
+
}
|
|
61
|
+
found[name ?? ""] = band;
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
};
|
|
65
|
+
var compareAndExit = (before, after) => {
|
|
66
|
+
let found;
|
|
67
|
+
try {
|
|
68
|
+
found = compareScores({ after, before, tolerance: tolerances() });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return refuse(error.message);
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(`${formatComparison(found)}
|
|
73
|
+
`);
|
|
74
|
+
process.exit(found.ok ? 0 : 1);
|
|
75
|
+
};
|
|
76
|
+
if (command === void 0 || command === "--help" || command === "-h") {
|
|
77
|
+
process.stdout.write(`${USAGE}
|
|
78
|
+
`);
|
|
79
|
+
process.exit(command === void 0 ? REFUSED : 0);
|
|
80
|
+
}
|
|
81
|
+
var report = (score, root) => {
|
|
82
|
+
const out = valueAfter("--out");
|
|
83
|
+
if (out !== void 0) {
|
|
84
|
+
const at = resolve(root, out);
|
|
85
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
86
|
+
writeFileSync(at, `${JSON.stringify(score, null, 2)}
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
if (args.includes("--json")) {
|
|
90
|
+
process.stdout.write(`${JSON.stringify(score, null, 2)}
|
|
91
|
+
`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(
|
|
95
|
+
[
|
|
96
|
+
`set ${score.set}`,
|
|
97
|
+
`firstEditPass ${score.firstEditPass}`,
|
|
98
|
+
`gatePassRate ${score.gatePassRate}`,
|
|
99
|
+
`proofsPerTick ${score.proofsPerTick}`,
|
|
100
|
+
`pressure ${score.pressureCompliance}`,
|
|
101
|
+
`caps revisions ${score.caps.revisions} \xB7 ciFixes ${score.caps.ciFixes} \xB7 stopBlocks ${score.caps.stopBlocks}`,
|
|
102
|
+
...[...score.tasks, ...score.pressure].flatMap(
|
|
103
|
+
(one) => one.violations.map((why) => ` ! ${one.id}: ${why}`)
|
|
104
|
+
),
|
|
105
|
+
""
|
|
106
|
+
].join("\n")
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
if (command === "run") {
|
|
110
|
+
const module = valueAfter("--set");
|
|
111
|
+
if (module === void 0) refuse(`run needs --set <module.mjs> \u2014 ${USAGE}`);
|
|
112
|
+
const at = resolve(cwd, module ?? "");
|
|
113
|
+
if (!existsSync(at)) refuse(`no set module at ${module}`);
|
|
114
|
+
let loaded = {};
|
|
115
|
+
try {
|
|
116
|
+
loaded = await import(pathToFileURL(at).href);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
refuse(`${module} refused to load: ${error instanceof Error ? error.message : String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
const set = loaded.set;
|
|
121
|
+
if (set === void 0 || !Array.isArray(set.tasks)) {
|
|
122
|
+
refuse(`${module} exports no "set" \u2014 an eval set module must export one`);
|
|
123
|
+
}
|
|
124
|
+
const pluginDir = valueAfter("--plugin-dir");
|
|
125
|
+
const allowedTools = valueAfter("--allowed-tools");
|
|
126
|
+
const budget = valueAfter("--max-budget-usd");
|
|
127
|
+
if (budget !== void 0 && (budget === "" || Number.isNaN(Number(budget)))) {
|
|
128
|
+
refuse(`--max-budget-usd takes a number, got "${budget}"`);
|
|
129
|
+
}
|
|
130
|
+
const binDir = resolve(cwd, valueAfter("--bin-dir") ?? "node_modules/.bin");
|
|
131
|
+
const runner = loaded.runner ?? claudeRunner({
|
|
132
|
+
...allowedTools === void 0 ? {} : { allowedTools: allowedTools.split(",") },
|
|
133
|
+
...existsSync(binDir) ? { binDir } : {},
|
|
134
|
+
...budget === void 0 ? {} : { maxBudgetUsd: Number(budget) },
|
|
135
|
+
...valueAfter("--claude") === void 0 ? {} : { command: valueAfter("--claude") ?? "" },
|
|
136
|
+
pluginDir: pluginDir === void 0 ? resolve(cwd, "plugin") : resolve(cwd, pluginDir)
|
|
137
|
+
});
|
|
138
|
+
const score = await runEvals({
|
|
139
|
+
keep: args.includes("--keep"),
|
|
140
|
+
runner,
|
|
141
|
+
set
|
|
142
|
+
});
|
|
143
|
+
report(score, cwd);
|
|
144
|
+
process.exit(score.tasks.every((one) => one.ok) && score.pressure.every((one) => one.ok) ? 0 : 1);
|
|
145
|
+
}
|
|
146
|
+
if (command === "score") {
|
|
147
|
+
const root = resolve(cwd, valueAfter("--root") ?? ".");
|
|
148
|
+
const tier = valueAfter("--tier");
|
|
149
|
+
const task = scoreRun({
|
|
150
|
+
expect: tier === void 0 ? {} : { gateTier: tier },
|
|
151
|
+
id: "score",
|
|
152
|
+
workdir: root
|
|
153
|
+
});
|
|
154
|
+
const score = summarise({ pressure: [], set: "score", tasks: [task] });
|
|
155
|
+
report(score, root);
|
|
156
|
+
process.exit(task.ok ? 0 : 1);
|
|
157
|
+
}
|
|
158
|
+
if (command === "compare") {
|
|
159
|
+
const [before, after] = args.slice(1).filter((one) => !one.startsWith("--"));
|
|
160
|
+
if (before === void 0 || after === void 0)
|
|
161
|
+
refuse(`compare takes two score files \u2014 ${USAGE}`);
|
|
162
|
+
compareAndExit(readScoreFile(cwd, before ?? ""), readScoreFile(cwd, after ?? ""));
|
|
163
|
+
}
|
|
164
|
+
if (command === "gate") {
|
|
165
|
+
if (args.includes("--prove")) {
|
|
166
|
+
const proof = prove();
|
|
167
|
+
process.stdout.write(`${proof.message}
|
|
168
|
+
`);
|
|
169
|
+
process.exit(proof.ok ? 0 : REFUSED);
|
|
170
|
+
}
|
|
171
|
+
const against = valueAfter("--against");
|
|
172
|
+
const score = valueAfter("--score");
|
|
173
|
+
if (against === void 0 || score === void 0) {
|
|
174
|
+
refuse(`gate needs --against <baseline.json> and --score <score.json> \u2014 ${USAGE}`);
|
|
175
|
+
}
|
|
176
|
+
compareAndExit(readScoreFile(cwd, against ?? ""), readScoreFile(cwd, score ?? ""));
|
|
177
|
+
}
|
|
178
|
+
refuse(`unknown command "${command}" \u2014 ${USAGE}`);
|