firegoat 0.28.0 → 0.30.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/README.md +2 -0
- package/dist/cli.js +355 -251
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ In a terminal it asks where to install the firegoat-onboarding skill (global by
|
|
|
22
22
|
npx firegoat@latest eval
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Run by an agent, eval ends with a results block to paste as-is (also in `.firegoat/runs/<runId>/results.txt`): ask it to show you the results table.
|
|
26
|
+
|
|
25
27
|
## Example
|
|
26
28
|
|
|
27
29
|
```yaml
|
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.30.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7265,8 +7265,146 @@ import { homedir } from "node:os";
|
|
|
7265
7265
|
import { join as join3 } from "node:path";
|
|
7266
7266
|
|
|
7267
7267
|
// src/home.ts
|
|
7268
|
+
import { execFile } from "node:child_process";
|
|
7268
7269
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7269
7270
|
import { join as join2 } from "node:path";
|
|
7271
|
+
import { promisify } from "node:util";
|
|
7272
|
+
|
|
7273
|
+
// src/process.ts
|
|
7274
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7275
|
+
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7276
|
+
var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
|
|
7277
|
+
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7278
|
+
function hostEnv() {
|
|
7279
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7280
|
+
}
|
|
7281
|
+
var GRACE_MS = 5000;
|
|
7282
|
+
var liveGroups = new Set;
|
|
7283
|
+
var signalsHooked = false;
|
|
7284
|
+
function hookSignals() {
|
|
7285
|
+
if (signalsHooked)
|
|
7286
|
+
return;
|
|
7287
|
+
signalsHooked = true;
|
|
7288
|
+
const onSignal = (signal) => {
|
|
7289
|
+
for (const g of liveGroups)
|
|
7290
|
+
try {
|
|
7291
|
+
process.kill(-g, "SIGKILL");
|
|
7292
|
+
} catch {}
|
|
7293
|
+
process.kill(process.pid, signal);
|
|
7294
|
+
};
|
|
7295
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
7296
|
+
process.once(s, onSignal);
|
|
7297
|
+
}
|
|
7298
|
+
function forgetGroup(pgid) {
|
|
7299
|
+
liveGroups.delete(pgid);
|
|
7300
|
+
}
|
|
7301
|
+
function processTree(pid) {
|
|
7302
|
+
const children = new Map;
|
|
7303
|
+
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
7304
|
+
encoding: "utf8"
|
|
7305
|
+
});
|
|
7306
|
+
for (const line of ps.split(`
|
|
7307
|
+
`)) {
|
|
7308
|
+
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
7309
|
+
if (!child || parent === undefined)
|
|
7310
|
+
continue;
|
|
7311
|
+
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
7312
|
+
}
|
|
7313
|
+
const tree = [pid];
|
|
7314
|
+
for (let i = 0;i < tree.length; i++)
|
|
7315
|
+
tree.push(...children.get(tree[i]) ?? []);
|
|
7316
|
+
return tree;
|
|
7317
|
+
}
|
|
7318
|
+
function killTree(pid, group) {
|
|
7319
|
+
if (group)
|
|
7320
|
+
try {
|
|
7321
|
+
process.kill(-pid, "SIGKILL");
|
|
7322
|
+
} catch {}
|
|
7323
|
+
let tree = [pid];
|
|
7324
|
+
let problem;
|
|
7325
|
+
try {
|
|
7326
|
+
tree = processTree(pid);
|
|
7327
|
+
} catch (e) {
|
|
7328
|
+
problem = e.message;
|
|
7329
|
+
}
|
|
7330
|
+
for (const p of tree)
|
|
7331
|
+
try {
|
|
7332
|
+
process.kill(p, "SIGKILL");
|
|
7333
|
+
} catch {}
|
|
7334
|
+
return problem;
|
|
7335
|
+
}
|
|
7336
|
+
function run(opts) {
|
|
7337
|
+
return new Promise((resolve) => {
|
|
7338
|
+
const detached = opts.detached === true;
|
|
7339
|
+
if (detached)
|
|
7340
|
+
hookSignals();
|
|
7341
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
7342
|
+
cwd: opts.cwd,
|
|
7343
|
+
env: opts.env,
|
|
7344
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7345
|
+
detached
|
|
7346
|
+
});
|
|
7347
|
+
if (child.pid !== undefined) {
|
|
7348
|
+
if (detached)
|
|
7349
|
+
liveGroups.add(child.pid);
|
|
7350
|
+
opts.onSpawn?.(child.pid);
|
|
7351
|
+
}
|
|
7352
|
+
let timedOut = false;
|
|
7353
|
+
let settled = false;
|
|
7354
|
+
const timer = setTimeout(() => {
|
|
7355
|
+
timedOut = true;
|
|
7356
|
+
if (child.pid === undefined)
|
|
7357
|
+
return;
|
|
7358
|
+
const problem = killTree(child.pid, detached);
|
|
7359
|
+
if (problem)
|
|
7360
|
+
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
7361
|
+
`));
|
|
7362
|
+
}, opts.timeoutMs);
|
|
7363
|
+
const done = (r) => {
|
|
7364
|
+
if (settled)
|
|
7365
|
+
return;
|
|
7366
|
+
settled = true;
|
|
7367
|
+
clearTimeout(timer);
|
|
7368
|
+
resolve(r);
|
|
7369
|
+
};
|
|
7370
|
+
child.stdout.on("data", opts.onStdout);
|
|
7371
|
+
child.stderr.on("data", opts.onStderr);
|
|
7372
|
+
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
7373
|
+
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
7374
|
+
child.on("close", (code) => done({ code, timedOut }));
|
|
7375
|
+
child.on("exit", (code) => setTimeout(() => {
|
|
7376
|
+
child.stdout.destroy();
|
|
7377
|
+
child.stderr.destroy();
|
|
7378
|
+
done({ code, timedOut });
|
|
7379
|
+
}, 2000).unref());
|
|
7380
|
+
});
|
|
7381
|
+
}
|
|
7382
|
+
async function runShell(opts) {
|
|
7383
|
+
opts.log.write(`$ ${opts.run}
|
|
7384
|
+
`);
|
|
7385
|
+
const r = await run({
|
|
7386
|
+
cmd: "/bin/sh",
|
|
7387
|
+
args: ["-c", opts.run],
|
|
7388
|
+
cwd: opts.cwd,
|
|
7389
|
+
env: opts.env,
|
|
7390
|
+
timeoutMs: opts.timeoutMs,
|
|
7391
|
+
onStdout: (c) => opts.log.write(c),
|
|
7392
|
+
onStderr: (c) => opts.log.write(c)
|
|
7393
|
+
});
|
|
7394
|
+
let error;
|
|
7395
|
+
if (r.error)
|
|
7396
|
+
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7397
|
+
else if (r.timedOut)
|
|
7398
|
+
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7399
|
+
else if (r.code === null)
|
|
7400
|
+
error = "killed by a signal";
|
|
7401
|
+
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7402
|
+
|
|
7403
|
+
`);
|
|
7404
|
+
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7407
|
+
// src/home.ts
|
|
7270
7408
|
function homeEnv(home) {
|
|
7271
7409
|
return {
|
|
7272
7410
|
HOME: home,
|
|
@@ -7287,6 +7425,23 @@ function makeRunHome(dir) {
|
|
|
7287
7425
|
`).join(""));
|
|
7288
7426
|
return { home, envFile };
|
|
7289
7427
|
}
|
|
7428
|
+
async function makeRunKeychain(home) {
|
|
7429
|
+
const keychain = join2(home, "Library", "Keychains", "login.keychain-db");
|
|
7430
|
+
const env = { ...hostEnv(), HOME: home };
|
|
7431
|
+
mkdirSync(join2(home, "Library", "Keychains"), { recursive: true });
|
|
7432
|
+
for (const args of [
|
|
7433
|
+
["create-keychain", "-p", "", keychain],
|
|
7434
|
+
["set-keychain-settings", keychain]
|
|
7435
|
+
]) {
|
|
7436
|
+
try {
|
|
7437
|
+
await promisify(execFile)("security", args, { env, timeout: 30000 });
|
|
7438
|
+
} catch (e) {
|
|
7439
|
+
const err = e;
|
|
7440
|
+
return `could not create the run's login keychain (security ${args[0]} ${keychain}): ${err.stderr?.trim() || err.message}`;
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
return null;
|
|
7444
|
+
}
|
|
7290
7445
|
|
|
7291
7446
|
// src/harnesses/codex.ts
|
|
7292
7447
|
var FLAGS2 = [
|
|
@@ -7879,6 +8034,7 @@ function validHarnesses() {
|
|
|
7879
8034
|
// src/config.ts
|
|
7880
8035
|
var CONFIG_FILE = "firegoat.yaml";
|
|
7881
8036
|
var DEFAULT_TIMEOUT = 600;
|
|
8037
|
+
var DEFAULT_PARALLEL = 4;
|
|
7882
8038
|
|
|
7883
8039
|
class ConfigError extends Error {
|
|
7884
8040
|
}
|
|
@@ -7916,6 +8072,7 @@ function parseConfig(text) {
|
|
|
7916
8072
|
fail("(root)", "must be a mapping");
|
|
7917
8073
|
onlyKeys(raw, "(root)", [
|
|
7918
8074
|
"runs",
|
|
8075
|
+
"parallel",
|
|
7919
8076
|
"harness",
|
|
7920
8077
|
"model",
|
|
7921
8078
|
"build",
|
|
@@ -7927,6 +8084,7 @@ function parseConfig(text) {
|
|
|
7927
8084
|
"tests"
|
|
7928
8085
|
]);
|
|
7929
8086
|
const runs = posInt(raw.runs, "runs", 1);
|
|
8087
|
+
const parallel = posInt(raw.parallel, "parallel", DEFAULT_PARALLEL);
|
|
7930
8088
|
const harnesses = raw.harness === undefined ? [DEFAULT_HARNESS] : nameList(raw.harness, "harness", "a harness name");
|
|
7931
8089
|
harnesses.forEach((h, i) => {
|
|
7932
8090
|
if (!HARNESSES.has(h))
|
|
@@ -7990,6 +8148,7 @@ function parseConfig(text) {
|
|
|
7990
8148
|
});
|
|
7991
8149
|
return {
|
|
7992
8150
|
runs,
|
|
8151
|
+
parallel,
|
|
7993
8152
|
harnesses,
|
|
7994
8153
|
models,
|
|
7995
8154
|
build,
|
|
@@ -8173,6 +8332,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
8173
8332
|
import {
|
|
8174
8333
|
appendFileSync,
|
|
8175
8334
|
createWriteStream as createWriteStream2,
|
|
8335
|
+
existsSync as existsSync6,
|
|
8176
8336
|
mkdirSync as mkdirSync5,
|
|
8177
8337
|
writeFileSync as writeFileSync5
|
|
8178
8338
|
} from "node:fs";
|
|
@@ -8530,142 +8690,6 @@ function ensureFiregoatDir(root) {
|
|
|
8530
8690
|
// src/git.ts
|
|
8531
8691
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
8532
8692
|
import { basename, dirname } from "node:path";
|
|
8533
|
-
|
|
8534
|
-
// src/process.ts
|
|
8535
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
8536
|
-
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
8537
|
-
var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
|
|
8538
|
-
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
8539
|
-
function hostEnv() {
|
|
8540
|
-
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
8541
|
-
}
|
|
8542
|
-
var GRACE_MS = 5000;
|
|
8543
|
-
var liveGroups = new Set;
|
|
8544
|
-
var signalsHooked = false;
|
|
8545
|
-
function hookSignals() {
|
|
8546
|
-
if (signalsHooked)
|
|
8547
|
-
return;
|
|
8548
|
-
signalsHooked = true;
|
|
8549
|
-
const onSignal = (signal) => {
|
|
8550
|
-
for (const g of liveGroups)
|
|
8551
|
-
try {
|
|
8552
|
-
process.kill(-g, "SIGKILL");
|
|
8553
|
-
} catch {}
|
|
8554
|
-
process.kill(process.pid, signal);
|
|
8555
|
-
};
|
|
8556
|
-
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
8557
|
-
process.once(s, onSignal);
|
|
8558
|
-
}
|
|
8559
|
-
function forgetGroup(pgid) {
|
|
8560
|
-
liveGroups.delete(pgid);
|
|
8561
|
-
}
|
|
8562
|
-
function processTree(pid) {
|
|
8563
|
-
const children = new Map;
|
|
8564
|
-
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
8565
|
-
encoding: "utf8"
|
|
8566
|
-
});
|
|
8567
|
-
for (const line of ps.split(`
|
|
8568
|
-
`)) {
|
|
8569
|
-
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
8570
|
-
if (!child || parent === undefined)
|
|
8571
|
-
continue;
|
|
8572
|
-
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
8573
|
-
}
|
|
8574
|
-
const tree = [pid];
|
|
8575
|
-
for (let i = 0;i < tree.length; i++)
|
|
8576
|
-
tree.push(...children.get(tree[i]) ?? []);
|
|
8577
|
-
return tree;
|
|
8578
|
-
}
|
|
8579
|
-
function killTree(pid, group) {
|
|
8580
|
-
if (group)
|
|
8581
|
-
try {
|
|
8582
|
-
process.kill(-pid, "SIGKILL");
|
|
8583
|
-
} catch {}
|
|
8584
|
-
let tree = [pid];
|
|
8585
|
-
let problem;
|
|
8586
|
-
try {
|
|
8587
|
-
tree = processTree(pid);
|
|
8588
|
-
} catch (e) {
|
|
8589
|
-
problem = e.message;
|
|
8590
|
-
}
|
|
8591
|
-
for (const p of tree)
|
|
8592
|
-
try {
|
|
8593
|
-
process.kill(p, "SIGKILL");
|
|
8594
|
-
} catch {}
|
|
8595
|
-
return problem;
|
|
8596
|
-
}
|
|
8597
|
-
function run(opts) {
|
|
8598
|
-
return new Promise((resolve) => {
|
|
8599
|
-
const detached = opts.detached === true;
|
|
8600
|
-
if (detached)
|
|
8601
|
-
hookSignals();
|
|
8602
|
-
const child = spawn(opts.cmd, opts.args, {
|
|
8603
|
-
cwd: opts.cwd,
|
|
8604
|
-
env: opts.env,
|
|
8605
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8606
|
-
detached
|
|
8607
|
-
});
|
|
8608
|
-
if (child.pid !== undefined) {
|
|
8609
|
-
if (detached)
|
|
8610
|
-
liveGroups.add(child.pid);
|
|
8611
|
-
opts.onSpawn?.(child.pid);
|
|
8612
|
-
}
|
|
8613
|
-
let timedOut = false;
|
|
8614
|
-
let settled = false;
|
|
8615
|
-
const timer = setTimeout(() => {
|
|
8616
|
-
timedOut = true;
|
|
8617
|
-
if (child.pid === undefined)
|
|
8618
|
-
return;
|
|
8619
|
-
const problem = killTree(child.pid, detached);
|
|
8620
|
-
if (problem)
|
|
8621
|
-
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
8622
|
-
`));
|
|
8623
|
-
}, opts.timeoutMs);
|
|
8624
|
-
const done = (r) => {
|
|
8625
|
-
if (settled)
|
|
8626
|
-
return;
|
|
8627
|
-
settled = true;
|
|
8628
|
-
clearTimeout(timer);
|
|
8629
|
-
resolve(r);
|
|
8630
|
-
};
|
|
8631
|
-
child.stdout.on("data", opts.onStdout);
|
|
8632
|
-
child.stderr.on("data", opts.onStderr);
|
|
8633
|
-
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
8634
|
-
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
8635
|
-
child.on("close", (code) => done({ code, timedOut }));
|
|
8636
|
-
child.on("exit", (code) => setTimeout(() => {
|
|
8637
|
-
child.stdout.destroy();
|
|
8638
|
-
child.stderr.destroy();
|
|
8639
|
-
done({ code, timedOut });
|
|
8640
|
-
}, 2000).unref());
|
|
8641
|
-
});
|
|
8642
|
-
}
|
|
8643
|
-
async function runShell(opts) {
|
|
8644
|
-
opts.log.write(`$ ${opts.run}
|
|
8645
|
-
`);
|
|
8646
|
-
const r = await run({
|
|
8647
|
-
cmd: "/bin/sh",
|
|
8648
|
-
args: ["-c", opts.run],
|
|
8649
|
-
cwd: opts.cwd,
|
|
8650
|
-
env: opts.env,
|
|
8651
|
-
timeoutMs: opts.timeoutMs,
|
|
8652
|
-
onStdout: (c) => opts.log.write(c),
|
|
8653
|
-
onStderr: (c) => opts.log.write(c)
|
|
8654
|
-
});
|
|
8655
|
-
let error;
|
|
8656
|
-
if (r.error)
|
|
8657
|
-
error = `could not start /bin/sh: ${r.error.message}`;
|
|
8658
|
-
else if (r.timedOut)
|
|
8659
|
-
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
8660
|
-
else if (r.code === null)
|
|
8661
|
-
error = "killed by a signal";
|
|
8662
|
-
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
8663
|
-
|
|
8664
|
-
`);
|
|
8665
|
-
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
8666
|
-
}
|
|
8667
|
-
|
|
8668
|
-
// src/git.ts
|
|
8669
8693
|
function git(dir, args) {
|
|
8670
8694
|
const r = spawnSync3("git", args, {
|
|
8671
8695
|
cwd: dir,
|
|
@@ -8834,6 +8858,9 @@ firegoat.yaml, committed.
|
|
|
8834
8858
|
|
|
8835
8859
|
firegoat.yaml reference:
|
|
8836
8860
|
runs: 1 Runs per test
|
|
8861
|
+
parallel: 4 Runs at once (default 4; 1 runs them one after
|
|
8862
|
+
another, e.g. when tests share one cloud
|
|
8863
|
+
account); eval --parallel overrides
|
|
8837
8864
|
harness: <name> | [<name>, ...]
|
|
8838
8865
|
The agent CLI: claude-code (default), codex-cli
|
|
8839
8866
|
or grok-cli.
|
|
@@ -8946,7 +8973,11 @@ firegoat.yaml reference:
|
|
|
8946
8973
|
Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
8947
8974
|
transcript, greppable line by line (format: eval --help, Transcript). So
|
|
8948
8975
|
'^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
|
|
8949
|
-
$FIREGOAT_SESSION (the harness's full session file, JSON lines).
|
|
8976
|
+
$FIREGOAT_SESSION (the harness's full session file, JSON lines). cleanup gets
|
|
8977
|
+
$FIREGOAT_TRANSCRIPT once the agent has run (also after a claude error or a
|
|
8978
|
+
timeout), and $FIREGOAT_SESSION when the harness saved a session file; neither
|
|
8979
|
+
is set when install failed before the agent started, so a cleanup that reads
|
|
8980
|
+
them should fail on an unset one: grep ... "\${FIREGOAT_TRANSCRIPT:?}".
|
|
8950
8981
|
|
|
8951
8982
|
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
8952
8983
|
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
@@ -9102,10 +9133,13 @@ Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
|
|
|
9102
9133
|
$FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
|
|
9103
9134
|
collide. claude itself keeps your real HOME (its login is there) and sources
|
|
9104
9135
|
home.env next to it (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
|
|
9105
|
-
checks and cleanup get the run's HOME directly; build keeps yours.
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9136
|
+
checks and cleanup get the run's HOME directly; build keeps yours. On macOS the run's
|
|
9137
|
+
home has its own empty, password-less login keychain (Library/Keychains), so a
|
|
9138
|
+
browser the agent starts doesn't ask you for one; your keychain is never touched.
|
|
9139
|
+
Limits: claude's own Read/Write tools expand ~ to your real home; getpwuid lookups
|
|
9140
|
+
(os.userInfo, python pwd) still see it; keychain logins (gh, docker, git credential
|
|
9141
|
+
helpers) look logged out inside a run, so log tools in from install: or with env:
|
|
9142
|
+
tokens; the
|
|
9109
9143
|
fresh home has no .gitconfig, so set user.name/user.email in install: if the agent
|
|
9110
9144
|
commits; claude's shell snapshot still sources your real ~/.zshrc.
|
|
9111
9145
|
|
|
@@ -9129,14 +9163,16 @@ firegoat.yaml, fixtures and build: stay in the current folder.
|
|
|
9129
9163
|
--model NAME The harness's --model, replacing model: in firegoat.yaml;
|
|
9130
9164
|
repeat it (--model a --model b) to run each test runs times
|
|
9131
9165
|
per model
|
|
9132
|
-
--parallel N Runs at once (
|
|
9166
|
+
--parallel N Runs at once (overrides parallel: in firegoat.yaml;
|
|
9167
|
+
default 4; 1 runs them one after another).
|
|
9133
9168
|
Each run has its own HOME; state outside it (a fixed port,
|
|
9134
9169
|
/tmp paths) can still collide: use 1 then
|
|
9135
9170
|
--var KEY=value Fills {{vars.KEY}} (prompts, checks, build, install,
|
|
9136
9171
|
cleanup); env for build, install, checks and cleanup
|
|
9137
9172
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
9138
9173
|
stops early); progress goes to stderr. The verdict comes
|
|
9139
|
-
last, after runs: totals {pass, fail, error},
|
|
9174
|
+
last, after runs: totals {pass, fail, error}, tier
|
|
9175
|
+
(S/A/B/C, as printed; null when no run was graded), status
|
|
9140
9176
|
("pass", "fail" or "error" = not graded) and exitCode
|
|
9141
9177
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
9142
9178
|
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
@@ -9167,6 +9203,14 @@ says "N uploads failed" on stderr. A file over 20 MiB fails that run's upload.
|
|
|
9167
9203
|
Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
9168
9204
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
9169
9205
|
|
|
9206
|
+
Results block: without a terminal (an agent, a pipe, CI) and without --json, eval
|
|
9207
|
+
ends with the terminal's final timeline as one plain block (no color, at most 100
|
|
9208
|
+
columns): the logo header, a row per run (status, a ramp of one block per agent turn
|
|
9209
|
+
ending ▲ pass, ╳ fail or ╌ not graded, checks passed, cost, turns, time), its failed
|
|
9210
|
+
checks and trace URL, the totals and the tier ("GOATED · tier S" when all pass). It
|
|
9211
|
+
sits between two lines of ═, after a "Results (paste the block ...)" line; the last
|
|
9212
|
+
line says where it is saved, results.txt. Show it to your user as-is in a code block.
|
|
9213
|
+
|
|
9170
9214
|
Output: .firegoat/runs/<runId>/
|
|
9171
9215
|
out A symlink to $FIREGOAT_OUT,
|
|
9172
9216
|
~/.firegoat/runs/<hash>/<random>/out
|
|
@@ -9174,7 +9218,8 @@ Output: .firegoat/runs/<runId>/
|
|
|
9174
9218
|
ran), git {branch, commit, dirty} (no git outside a
|
|
9175
9219
|
repo), harnesses, upload {origin, email} (null when
|
|
9176
9220
|
not uploading), runs (every run.json), then the
|
|
9177
|
-
verdict: totals, status and exitCode
|
|
9221
|
+
verdict: totals, tier, status and exitCode
|
|
9222
|
+
results.txt The results block, also with --json or in a terminal
|
|
9178
9223
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
9179
9224
|
commit, harnesses, passed, failed, notGraded, cost (null when
|
|
9180
9225
|
any run's cost is unknown), uploaded, uploadFailed
|
|
@@ -9191,7 +9236,8 @@ Output: .firegoat/runs/<runId>/
|
|
|
9191
9236
|
claudeSession (the session.jsonl uploaded: claude's
|
|
9192
9237
|
own file, else <test>/<n>/session.jsonl made from the
|
|
9193
9238
|
stream) and sessionId (its id, the trace's), stderr,
|
|
9194
|
-
checksLog, cleanupLog;
|
|
9239
|
+
checksLog, cleanupLog; turns and cost (null when the
|
|
9240
|
+
harness reports none), durationMs, startedAt; upload ({status: "uploaded",
|
|
9195
9241
|
url} | {status: "failed", error} | {status:
|
|
9196
9242
|
"skipped", reason}, null when not uploading)
|
|
9197
9243
|
<test>/<n>/transcript.txt What the agent did, below
|
|
@@ -10276,17 +10322,9 @@ function plainUI(write) {
|
|
|
10276
10322
|
}
|
|
10277
10323
|
};
|
|
10278
10324
|
}
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
let build = null;
|
|
10283
|
-
let authText = "";
|
|
10284
|
-
const notes = [];
|
|
10285
|
-
let frame = 0;
|
|
10286
|
-
let drawn = 0;
|
|
10287
|
-
let finished = null;
|
|
10288
|
-
const width = () => Math.min(out.columns || 80, 100);
|
|
10289
|
-
const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
10325
|
+
var cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
10326
|
+
function timeline(t, p, w) {
|
|
10327
|
+
const { rows, build, finished, frame } = t;
|
|
10290
10328
|
const statusWord = (r) => {
|
|
10291
10329
|
switch (r.state) {
|
|
10292
10330
|
case "pass":
|
|
@@ -10315,77 +10353,109 @@ function ttyUI(out, opts) {
|
|
|
10315
10353
|
return p(C.gray, "○");
|
|
10316
10354
|
}
|
|
10317
10355
|
};
|
|
10318
|
-
const
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
10375
|
-
return out;
|
|
10376
|
-
}
|
|
10377
|
-
const tr = tier(rows);
|
|
10378
|
-
const goated = c.pass === rows.length && rows.length > 0;
|
|
10379
|
-
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10380
|
-
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
10381
|
-
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10382
|
-
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10383
|
-
const turns = totalTurns(rows);
|
|
10384
|
-
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
10385
|
-
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10386
|
-
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10356
|
+
const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
|
|
10357
|
+
const c = counts(rows);
|
|
10358
|
+
const doneCount = c.pass + c.fail + c.error;
|
|
10359
|
+
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
10360
|
+
const out = [
|
|
10361
|
+
...logoHeader(p, `firegoat ${t.version} eval · ${t.project}`, p(finished ? C.green : C.ember, badge), w),
|
|
10362
|
+
rule
|
|
10363
|
+
];
|
|
10364
|
+
if (t.authText)
|
|
10365
|
+
out.push(p(C.gray, ` Auth ${t.authText}`));
|
|
10366
|
+
for (const n of t.notes)
|
|
10367
|
+
out.push(p(C.gray, ` ${cut(n, w - 2)}`));
|
|
10368
|
+
if (build) {
|
|
10369
|
+
const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
10370
|
+
out.push(` ${build.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} Build${" ".repeat(Math.max(1, w - 20))}${mark}${build.seconds !== undefined ? ` ${build.seconds}s` : ""}`);
|
|
10371
|
+
}
|
|
10372
|
+
const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
|
|
10373
|
+
const harnessCols = column((r) => r.harness?.length ?? 0);
|
|
10374
|
+
const modelCols = column((r) => r.model?.length ?? 0);
|
|
10375
|
+
const detailCols = w - 1 - (47 + (harnessCols > 0 ? harnessCols + 1 : 0) + (modelCols > 0 ? modelCols + 1 : 0));
|
|
10376
|
+
rows.forEach((r, i) => {
|
|
10377
|
+
out.push(p(C.gray, " │"));
|
|
10378
|
+
const num = String(i + 1).padStart(2, "0");
|
|
10379
|
+
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
10380
|
+
const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
|
|
10381
|
+
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
10382
|
+
let detail = "";
|
|
10383
|
+
if (r.state === "running")
|
|
10384
|
+
detail = p(C.gray, `Turn ${r.turns}`);
|
|
10385
|
+
else if (r.state === "pass" || r.state === "fail")
|
|
10386
|
+
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, cut(usage(r), Math.max(1, detailCols - 12)))}`;
|
|
10387
|
+
else if (r.state === "error")
|
|
10388
|
+
detail = p(C.gray, cut(usage(r), Math.max(1, detailCols)));
|
|
10389
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
10390
|
+
if (r.state === "running" && r.command)
|
|
10391
|
+
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
10392
|
+
if (r.state === "fail")
|
|
10393
|
+
for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
|
|
10394
|
+
out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
|
|
10395
|
+
out.push(p(C.gray, ` │ ${cut(ck.error ?? `exit ${ck.code}`, w - 14)}`));
|
|
10396
|
+
}
|
|
10397
|
+
if (r.state === "error")
|
|
10398
|
+
out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
|
|
10399
|
+
if (r.upload === "uploading")
|
|
10400
|
+
out.push(p(C.gray, " │ ↑ Uploading…"));
|
|
10401
|
+
else if (r.upload?.status === "uploaded")
|
|
10402
|
+
out.push(` ${p(C.gray, "│")} ${p(C.cyan, `↑ ${r.upload.url}`)}`);
|
|
10403
|
+
else if (r.upload)
|
|
10404
|
+
out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
|
|
10405
|
+
});
|
|
10406
|
+
out.push(rule);
|
|
10407
|
+
if (!finished) {
|
|
10408
|
+
const track = Math.max(10, w - 34);
|
|
10409
|
+
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
10410
|
+
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
10411
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
10387
10412
|
return out;
|
|
10388
|
-
}
|
|
10413
|
+
}
|
|
10414
|
+
const tr = tier(rows);
|
|
10415
|
+
const goated = c.pass === rows.length && rows.length > 0;
|
|
10416
|
+
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10417
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
10418
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10419
|
+
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10420
|
+
const turns = totalTurns(rows);
|
|
10421
|
+
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
10422
|
+
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10423
|
+
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10424
|
+
return out;
|
|
10425
|
+
}
|
|
10426
|
+
var RESULTS_WIDTH = 100;
|
|
10427
|
+
function resultsBlock(t) {
|
|
10428
|
+
return timeline({
|
|
10429
|
+
project: t.project,
|
|
10430
|
+
version: t.version,
|
|
10431
|
+
rows: t.rows,
|
|
10432
|
+
build: null,
|
|
10433
|
+
authText: "",
|
|
10434
|
+
notes: [],
|
|
10435
|
+
finished: { runId: t.runId, runsDir: t.runsDir, seconds: t.seconds },
|
|
10436
|
+
frame: 0
|
|
10437
|
+
}, painter(false), RESULTS_WIDTH);
|
|
10438
|
+
}
|
|
10439
|
+
function ttyUI(out, opts) {
|
|
10440
|
+
const p = painter(opts.color);
|
|
10441
|
+
let rows = [];
|
|
10442
|
+
let build = null;
|
|
10443
|
+
let authText = "";
|
|
10444
|
+
const notes = [];
|
|
10445
|
+
let frame = 0;
|
|
10446
|
+
let drawn = 0;
|
|
10447
|
+
let finished = null;
|
|
10448
|
+
const width = () => Math.min(out.columns || 80, 100);
|
|
10449
|
+
const lines = () => timeline({
|
|
10450
|
+
project: opts.project,
|
|
10451
|
+
version: opts.version,
|
|
10452
|
+
rows,
|
|
10453
|
+
build,
|
|
10454
|
+
authText,
|
|
10455
|
+
notes,
|
|
10456
|
+
finished,
|
|
10457
|
+
frame
|
|
10458
|
+
}, p, width());
|
|
10389
10459
|
const rowsOf = (line) => {
|
|
10390
10460
|
const visible = line.replace(ANSI, "");
|
|
10391
10461
|
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
@@ -10543,9 +10613,9 @@ async function uploadRun(t, r) {
|
|
|
10543
10613
|
run: clip(c.run, MAX_TEXT, "…"),
|
|
10544
10614
|
code: c.code
|
|
10545
10615
|
})),
|
|
10546
|
-
num_turns: r.
|
|
10616
|
+
num_turns: r.turns,
|
|
10547
10617
|
cost: r.cost,
|
|
10548
|
-
duration_ms: r.
|
|
10618
|
+
duration_ms: r.durationMs,
|
|
10549
10619
|
harness: r.harness,
|
|
10550
10620
|
model,
|
|
10551
10621
|
...git && { git }
|
|
@@ -10624,7 +10694,6 @@ function parseArgs(args) {
|
|
|
10624
10694
|
names: [],
|
|
10625
10695
|
models: [],
|
|
10626
10696
|
harnesses: [],
|
|
10627
|
-
parallel: 4,
|
|
10628
10697
|
json: false,
|
|
10629
10698
|
noUpload: false,
|
|
10630
10699
|
dryRun: false,
|
|
@@ -10835,9 +10904,9 @@ async function runOne(opts) {
|
|
|
10835
10904
|
cleanup: [],
|
|
10836
10905
|
cleanupLog: join10(dir, "cleanup.txt"),
|
|
10837
10906
|
leftover: [],
|
|
10838
|
-
|
|
10907
|
+
turns: null,
|
|
10839
10908
|
cost: null,
|
|
10840
|
-
|
|
10909
|
+
durationMs: 0,
|
|
10841
10910
|
startedAt: new Date(started).toISOString(),
|
|
10842
10911
|
upload: null
|
|
10843
10912
|
};
|
|
@@ -10858,13 +10927,17 @@ async function runOne(opts) {
|
|
|
10858
10927
|
r.cleanup = await runAllSteps({
|
|
10859
10928
|
commands: steps.cleanup,
|
|
10860
10929
|
cwd: workdir,
|
|
10861
|
-
env:
|
|
10930
|
+
env: {
|
|
10931
|
+
...stepEnv,
|
|
10932
|
+
...existsSync6(r.transcript) && { FIREGOAT_TRANSCRIPT: r.transcript },
|
|
10933
|
+
...r.session !== null && { FIREGOAT_SESSION: r.session }
|
|
10934
|
+
},
|
|
10862
10935
|
logPath: r.cleanupLog
|
|
10863
10936
|
});
|
|
10864
10937
|
r.leftover = await stopLeftovers({ group: agent.group, workdir });
|
|
10865
10938
|
r.status = status;
|
|
10866
10939
|
r.reason = reason;
|
|
10867
|
-
r.
|
|
10940
|
+
r.durationMs = Date.now() - started;
|
|
10868
10941
|
writeRunJson(r);
|
|
10869
10942
|
return r;
|
|
10870
10943
|
}
|
|
@@ -10874,6 +10947,11 @@ function withoutOldPwd(env) {
|
|
|
10874
10947
|
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
10875
10948
|
const { test, n } = opts;
|
|
10876
10949
|
const { dir, workdir } = r;
|
|
10950
|
+
if (process.platform === "darwin") {
|
|
10951
|
+
const error = await makeRunKeychain(r.home);
|
|
10952
|
+
if (error)
|
|
10953
|
+
return ["error", `not graded: ${error}`];
|
|
10954
|
+
}
|
|
10877
10955
|
if (steps.install.length > 0) {
|
|
10878
10956
|
const error = await runSteps({
|
|
10879
10957
|
what: "install",
|
|
@@ -10947,7 +11025,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
|
10947
11025
|
final: outcome.final,
|
|
10948
11026
|
workdir
|
|
10949
11027
|
});
|
|
10950
|
-
r.
|
|
11028
|
+
r.turns = outcome.turns;
|
|
10951
11029
|
r.cost = outcome.cost;
|
|
10952
11030
|
if (!outcome.ok)
|
|
10953
11031
|
return ["error", `not graded: ${outcome.reason}`];
|
|
@@ -10998,6 +11076,16 @@ function uploadedSummary(origin, results, runDir) {
|
|
|
10998
11076
|
for (const u of urls)
|
|
10999
11077
|
console.log(` ${u}`);
|
|
11000
11078
|
}
|
|
11079
|
+
var RESULTS_EDGE = "═".repeat(RESULTS_WIDTH);
|
|
11080
|
+
function printResults(block, file) {
|
|
11081
|
+
console.log("");
|
|
11082
|
+
console.log("Results (paste the block between the ═ lines as-is, in a code block):");
|
|
11083
|
+
console.log(RESULTS_EDGE);
|
|
11084
|
+
for (const line of block)
|
|
11085
|
+
console.log(line);
|
|
11086
|
+
console.log(RESULTS_EDGE);
|
|
11087
|
+
console.log(`Results block saved to ${file}`);
|
|
11088
|
+
}
|
|
11001
11089
|
async function evalCommand(args, root) {
|
|
11002
11090
|
if (args.includes("--help") || args.includes("-h")) {
|
|
11003
11091
|
console.log(EVAL_HELP);
|
|
@@ -11045,6 +11133,7 @@ async function evalCommand(args, root) {
|
|
|
11045
11133
|
tests = tests.filter((t) => opts.names.includes(t.name));
|
|
11046
11134
|
}
|
|
11047
11135
|
const runs = opts.runs ?? config.runs;
|
|
11136
|
+
const parallel = opts.parallel ?? config.parallel;
|
|
11048
11137
|
const models = opts.models.length > 0 ? opts.models : config.models;
|
|
11049
11138
|
const harnessNames = opts.harnesses.length > 0 ? opts.harnesses : config.harnesses;
|
|
11050
11139
|
if (opts.harnessBin !== undefined && harnessNames.length !== 1)
|
|
@@ -11055,7 +11144,7 @@ async function evalCommand(args, root) {
|
|
|
11055
11144
|
testCount: tests.length,
|
|
11056
11145
|
runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
|
|
11057
11146
|
modelCount: models.length,
|
|
11058
|
-
parallel
|
|
11147
|
+
parallel
|
|
11059
11148
|
});
|
|
11060
11149
|
const vars = { ...config.vars, ...opts.vars };
|
|
11061
11150
|
const missing = [
|
|
@@ -11215,11 +11304,11 @@ async function evalCommand(args, root) {
|
|
|
11215
11304
|
results[i] = r;
|
|
11216
11305
|
Object.assign(row, {
|
|
11217
11306
|
state: r.status,
|
|
11218
|
-
reportedTurns: r.
|
|
11307
|
+
reportedTurns: r.turns,
|
|
11219
11308
|
reason: r.reason,
|
|
11220
11309
|
checks: r.checks,
|
|
11221
11310
|
cost: r.cost,
|
|
11222
|
-
seconds: Math.round(r.
|
|
11311
|
+
seconds: Math.round(r.durationMs / 1000),
|
|
11223
11312
|
command: undefined
|
|
11224
11313
|
});
|
|
11225
11314
|
ui.update();
|
|
@@ -11237,7 +11326,7 @@ async function evalCommand(args, root) {
|
|
|
11237
11326
|
while (next < rows.length)
|
|
11238
11327
|
await runAt(next++);
|
|
11239
11328
|
};
|
|
11240
|
-
await Promise.all(Array.from({ length: Math.min(
|
|
11329
|
+
await Promise.all(Array.from({ length: Math.min(parallel, rows.length) }, worker));
|
|
11241
11330
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
11242
11331
|
for (const r of results)
|
|
11243
11332
|
totals[r.status]++;
|
|
@@ -11266,6 +11355,7 @@ async function evalCommand(args, root) {
|
|
|
11266
11355
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
11267
11356
|
runs: results,
|
|
11268
11357
|
totals,
|
|
11358
|
+
tier: tier(rows),
|
|
11269
11359
|
status,
|
|
11270
11360
|
exitCode
|
|
11271
11361
|
};
|
|
@@ -11286,12 +11376,8 @@ async function evalCommand(args, root) {
|
|
|
11286
11376
|
uploadFailed: uploadsFailed
|
|
11287
11377
|
})}
|
|
11288
11378
|
`);
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
runsDir: runDir,
|
|
11292
|
-
rows,
|
|
11293
|
-
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
11294
|
-
});
|
|
11379
|
+
const seconds = Math.round((Date.now() - evalStarted) / 1000);
|
|
11380
|
+
ui.done({ runId, runsDir: runDir, rows, seconds });
|
|
11295
11381
|
if (upload && !opts.json)
|
|
11296
11382
|
uploadedSummary(upload.origin, results, runDir);
|
|
11297
11383
|
for (const r of results)
|
|
@@ -11303,6 +11389,20 @@ async function evalCommand(args, root) {
|
|
|
11303
11389
|
console.error(`firegoat eval: Warning: ${r.test}#${r.n} left ${r.leftover.length} process${r.leftover.length === 1 ? "" : "es"} running, stopped after cleanup: ${r.leftover.map((l) => `${l.pid} ${l.command}`).join("; ")} (run.json leftover)`);
|
|
11304
11390
|
if (uploadsFailed > 0)
|
|
11305
11391
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
11392
|
+
const block = resultsBlock({
|
|
11393
|
+
project: basename2(root),
|
|
11394
|
+
version: package_default.version,
|
|
11395
|
+
rows,
|
|
11396
|
+
runId,
|
|
11397
|
+
runsDir: runDir,
|
|
11398
|
+
seconds
|
|
11399
|
+
});
|
|
11400
|
+
const resultsFile = join10(runDir, "results.txt");
|
|
11401
|
+
writeFileSync5(resultsFile, `${block.join(`
|
|
11402
|
+
`)}
|
|
11403
|
+
`);
|
|
11404
|
+
if (!opts.json && !stream.isTTY)
|
|
11405
|
+
printResults(block, resultsFile);
|
|
11306
11406
|
if (opts.json)
|
|
11307
11407
|
console.log(JSON.stringify(doc, null, 2));
|
|
11308
11408
|
if (status === "error")
|
|
@@ -11586,7 +11686,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
11586
11686
|
|
|
11587
11687
|
// src/init.ts
|
|
11588
11688
|
import { spawn as spawn4, spawnSync as spawnSync4 } from "node:child_process";
|
|
11589
|
-
import { existsSync as
|
|
11689
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
11590
11690
|
import { basename as basename3, join as join11 } from "node:path";
|
|
11591
11691
|
|
|
11592
11692
|
// src/login.ts
|
|
@@ -11900,6 +12000,10 @@ function template(product) {
|
|
|
11900
12000
|
# How many times to run each test.
|
|
11901
12001
|
runs: 1
|
|
11902
12002
|
|
|
12003
|
+
# Optional: how many runs at once (default 4). 1 runs them one after another,
|
|
12004
|
+
# e.g. when tests share one cloud account. eval --parallel N overrides it.
|
|
12005
|
+
# parallel: 1
|
|
12006
|
+
|
|
11903
12007
|
# Optional: the agent CLI each test runs on: claude-code (default), codex-cli or grok-cli.
|
|
11904
12008
|
# A list runs every test \`runs\` times per harness. eval --harness NAME overrides it.
|
|
11905
12009
|
# harness: [claude-code, codex-cli]
|
|
@@ -11998,7 +12102,7 @@ function copyToClipboard(text) {
|
|
|
11998
12102
|
});
|
|
11999
12103
|
}
|
|
12000
12104
|
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
12001
|
-
function
|
|
12105
|
+
function cut2(s, n) {
|
|
12002
12106
|
return s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}~`;
|
|
12003
12107
|
}
|
|
12004
12108
|
|
|
@@ -12013,8 +12117,8 @@ class Timeline {
|
|
|
12013
12117
|
line(mark, n, label, status = "") {
|
|
12014
12118
|
const head = ` ${mark} ${this.paint("90", String(n).padStart(2, "0"))} `;
|
|
12015
12119
|
if (!status)
|
|
12016
|
-
return head +
|
|
12017
|
-
return `${head}${label.padEnd(28)}${this.paint("90",
|
|
12120
|
+
return head + cut2(label, this.width - 9);
|
|
12121
|
+
return `${head}${label.padEnd(28)}${this.paint("90", cut2(status, this.width - 37))}`;
|
|
12018
12122
|
}
|
|
12019
12123
|
done(n, label, status) {
|
|
12020
12124
|
return this.line(this.paint("32", "●"), n, label, status);
|
|
@@ -12037,7 +12141,7 @@ class Timeline {
|
|
|
12037
12141
|
return select(block, options.length, (current) => [
|
|
12038
12142
|
this.line(this.paint("38;5;208", "◉"), n, question),
|
|
12039
12143
|
...options.map(([label, note], i) => {
|
|
12040
|
-
const text =
|
|
12144
|
+
const text = cut2(`${label.padEnd(cols)}${note}`, room);
|
|
12041
12145
|
return i === current ? `${Timeline.INDENT}${this.paint("38;5;208", `● ${text}`)}` : this.paint("90", `${Timeline.INDENT}○ ${text}`);
|
|
12042
12146
|
}),
|
|
12043
12147
|
this.paint("90", `${Timeline.INDENT}Up/down to move, enter to pick`)
|
|
@@ -12162,7 +12266,7 @@ ${INIT_HELP}`);
|
|
|
12162
12266
|
}
|
|
12163
12267
|
ensureFiregoatDir(cwd);
|
|
12164
12268
|
const configPath = join11(cwd, CONFIG_FILE);
|
|
12165
|
-
const keep =
|
|
12269
|
+
const keep = existsSync7(configPath) && !force;
|
|
12166
12270
|
if (!keep)
|
|
12167
12271
|
writeFileSync6(configPath, template(basename3(cwd)));
|
|
12168
12272
|
if (ui) {
|