firegoat 0.24.0 → 0.26.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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +1323 -483
  3. package/package.json +2 -2
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.24.0",
6950
+ version: "0.26.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
@@ -6976,7 +6976,7 @@ var package_default = {
6976
6976
  },
6977
6977
  scripts: {
6978
6978
  build: "bun build src/cli.ts --target node --outfile dist/cli.js",
6979
- test: "bun test",
6979
+ test: "bun test --timeout 30000",
6980
6980
  typecheck: "bun check",
6981
6981
  lint: "biome check .",
6982
6982
  format: "biome check --write .",
@@ -6987,7 +6987,7 @@ var package_default = {
6987
6987
  };
6988
6988
 
6989
6989
  // src/config.ts
6990
- import { existsSync, readFileSync } from "node:fs";
6990
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
6991
6991
 
6992
6992
  // node_modules/yaml/dist/index.js
6993
6993
  var composer = require_composer();
@@ -7035,6 +7035,830 @@ var $stringify = publicApi.stringify;
7035
7035
  var $visit = visit.visit;
7036
7036
  var $visitAsync = visit.visitAsync;
7037
7037
 
7038
+ // src/harnesses/claude.ts
7039
+ import { spawnSync } from "node:child_process";
7040
+ import { existsSync, readdirSync } from "node:fs";
7041
+ import { join } from "node:path";
7042
+ var FLAGS = [
7043
+ "--output-format",
7044
+ "stream-json",
7045
+ "--verbose",
7046
+ "--dangerously-skip-permissions",
7047
+ "--strict-mcp-config",
7048
+ "--setting-sources",
7049
+ "",
7050
+ "--disable-slash-commands"
7051
+ ];
7052
+ var SET = "firegoat sets it for every run";
7053
+ function parse(line) {
7054
+ try {
7055
+ const e = JSON.parse(line);
7056
+ return typeof e === "object" && e !== null ? e : undefined;
7057
+ } catch {
7058
+ return;
7059
+ }
7060
+ }
7061
+ function content(e) {
7062
+ const message = e?.message;
7063
+ return Array.isArray(message?.content) ? message.content : [];
7064
+ }
7065
+ function resultText(content) {
7066
+ if (typeof content === "string")
7067
+ return content;
7068
+ if (Array.isArray(content))
7069
+ return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
7070
+ `);
7071
+ return "";
7072
+ }
7073
+ function steps(events) {
7074
+ const out = [];
7075
+ for (const e of events) {
7076
+ if (e?.type === "assistant")
7077
+ for (const b of content(e)) {
7078
+ if (b.type === "tool_use")
7079
+ out.push({
7080
+ kind: "tool_call",
7081
+ name: typeof b.name === "string" ? b.name : "",
7082
+ input: typeof b.input === "object" && b.input !== null ? b.input : null
7083
+ });
7084
+ else if (b.type === "text" && typeof b.text === "string")
7085
+ out.push({ kind: "text", text: b.text });
7086
+ }
7087
+ else if (e?.type === "user") {
7088
+ for (const b of content(e))
7089
+ if (b.type === "tool_result")
7090
+ out.push({
7091
+ kind: "tool_result",
7092
+ isError: b.is_error === true,
7093
+ text: resultText(b.content).trimEnd()
7094
+ });
7095
+ }
7096
+ }
7097
+ return out;
7098
+ }
7099
+ function read(stdout, exited, timeoutMs) {
7100
+ const events = stdout.split(`
7101
+ `).filter((l) => l.trim() !== "").map(parse);
7102
+ let final = "";
7103
+ for (const e of events)
7104
+ if (e?.type === "result" && typeof e.result === "string")
7105
+ final = e.result;
7106
+ const result = events.findLast((e) => e?.type === "result");
7107
+ const trace = {
7108
+ steps: steps(events),
7109
+ final,
7110
+ turns: result?.num_turns ?? null,
7111
+ cost: result?.total_cost_usd ?? null
7112
+ };
7113
+ const init = events.find((e) => e?.type === "system" && e.subtype === "init");
7114
+ if (!init) {
7115
+ const said = stdout.trim().split(`
7116
+ `)[0]?.slice(0, 300);
7117
+ return {
7118
+ ...trace,
7119
+ reportedModel: null,
7120
+ version: null,
7121
+ ok: false,
7122
+ reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
7123
+ };
7124
+ }
7125
+ const reportedModel = typeof init.model === "string" ? init.model : null;
7126
+ const version = typeof init.claude_code_version === "string" ? init.claude_code_version : null;
7127
+ if (typeof init.session_id !== "string" || init.session_id === "")
7128
+ return {
7129
+ ...trace,
7130
+ reportedModel,
7131
+ version,
7132
+ ok: false,
7133
+ reason: "claude's init line has no session_id"
7134
+ };
7135
+ const sessionId = init.session_id;
7136
+ if (exited.timedOut)
7137
+ return {
7138
+ ...trace,
7139
+ reportedModel,
7140
+ version,
7141
+ ok: false,
7142
+ reason: `timed out after ${timeoutMs / 1000}s`,
7143
+ sessionId
7144
+ };
7145
+ if (!result)
7146
+ return {
7147
+ ...trace,
7148
+ reportedModel,
7149
+ version,
7150
+ ok: false,
7151
+ reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
7152
+ sessionId
7153
+ };
7154
+ if (result.is_error === true || result.subtype !== "success")
7155
+ return {
7156
+ ...trace,
7157
+ reportedModel,
7158
+ version,
7159
+ ok: false,
7160
+ reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
7161
+ sessionId
7162
+ };
7163
+ return { ...trace, reportedModel, version, ok: true, sessionId };
7164
+ }
7165
+ function progress(line) {
7166
+ const e = parse(line);
7167
+ if (e?.type !== "assistant")
7168
+ return;
7169
+ const p = {};
7170
+ for (const b of content(e)) {
7171
+ const cmd = b.input?.command;
7172
+ if (b.type === "tool_use" && typeof cmd === "string")
7173
+ p.command = cmd.split(`
7174
+ `)[0];
7175
+ }
7176
+ return p;
7177
+ }
7178
+ function checkLogin(opts) {
7179
+ const r = spawnSync(opts.bin, ["auth", "status"], {
7180
+ cwd: opts.cwd,
7181
+ env: opts.env,
7182
+ encoding: "utf8",
7183
+ timeout: 30000
7184
+ });
7185
+ if (r.error)
7186
+ return {
7187
+ loggedOut: false,
7188
+ reason: `could not run ${opts.bin}: ${r.error.message}`
7189
+ };
7190
+ let status;
7191
+ try {
7192
+ status = JSON.parse(r.stdout);
7193
+ } catch {}
7194
+ if (status?.loggedIn === true)
7195
+ return;
7196
+ if (status?.loggedIn === false)
7197
+ return {
7198
+ loggedOut: true,
7199
+ reason: "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again"
7200
+ };
7201
+ return {
7202
+ loggedOut: false,
7203
+ reason: `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`
7204
+ };
7205
+ }
7206
+ var claude = {
7207
+ name: "claude-code",
7208
+ bin: "claude",
7209
+ installHint: "Install Claude Code (https://claude.com/claude-code) or pass --harness-bin PATH.",
7210
+ configDirName: ".claude",
7211
+ reservedArgs: [
7212
+ [["-p", "--print", ...FLAGS.filter((f) => f.startsWith("-"))], SET],
7213
+ [["--model"], "choose the model with top-level model: (or eval --model)"],
7214
+ [
7215
+ ["--session-id", "--resume", "-r", "--continue", "-c"],
7216
+ "every run is a fresh session"
7217
+ ]
7218
+ ],
7219
+ uploads: true,
7220
+ args: ({ prompt, model, extraArgs }) => [
7221
+ "-p",
7222
+ prompt,
7223
+ ...FLAGS,
7224
+ ...model === undefined ? [] : ["--model", model],
7225
+ ...extraArgs
7226
+ ],
7227
+ env: ({ configDir, run }) => ({
7228
+ CLAUDE_CONFIG_DIR: configDir,
7229
+ CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
7230
+ ...run === undefined ? {} : { CLAUDE_ENV_FILE: run.homeEnvFile }
7231
+ }),
7232
+ authLabel: () => process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login",
7233
+ checkLogin,
7234
+ progress,
7235
+ read,
7236
+ sessionsDir: (configDir) => join(configDir, "projects"),
7237
+ findSession(configDir, sessionId) {
7238
+ const projects = join(configDir, "projects");
7239
+ if (!existsSync(projects))
7240
+ return null;
7241
+ for (const d of readdirSync(projects)) {
7242
+ const f = join(projects, d, `${sessionId}.jsonl`);
7243
+ if (existsSync(f))
7244
+ return f;
7245
+ }
7246
+ return null;
7247
+ }
7248
+ };
7249
+
7250
+ // src/harnesses/codex.ts
7251
+ import { spawnSync as spawnSync2 } from "node:child_process";
7252
+ import {
7253
+ existsSync as existsSync2,
7254
+ lstatSync,
7255
+ readdirSync as readdirSync2,
7256
+ readFileSync,
7257
+ symlinkSync,
7258
+ unlinkSync
7259
+ } from "node:fs";
7260
+ import { homedir } from "node:os";
7261
+ import { join as join3 } from "node:path";
7262
+
7263
+ // src/home.ts
7264
+ import { mkdirSync, writeFileSync } from "node:fs";
7265
+ import { join as join2 } from "node:path";
7266
+ function homeEnv(home) {
7267
+ return {
7268
+ HOME: home,
7269
+ XDG_CONFIG_HOME: join2(home, ".config"),
7270
+ XDG_CACHE_HOME: join2(home, ".cache"),
7271
+ XDG_DATA_HOME: join2(home, ".local", "share"),
7272
+ XDG_STATE_HOME: join2(home, ".local", "state")
7273
+ };
7274
+ }
7275
+ var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
7276
+ function makeRunHome(dir) {
7277
+ const home = join2(dir, "home");
7278
+ const env = homeEnv(home);
7279
+ for (const d of Object.values(env))
7280
+ mkdirSync(d, { recursive: true });
7281
+ const envFile = join2(dir, "home.env");
7282
+ writeFileSync(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
7283
+ `).join(""));
7284
+ return { home, envFile };
7285
+ }
7286
+
7287
+ // src/harnesses/codex.ts
7288
+ var FLAGS2 = [
7289
+ "--json",
7290
+ "--skip-git-repo-check",
7291
+ "--dangerously-bypass-approvals-and-sandbox"
7292
+ ];
7293
+ var NO_PROJECT_DOC = ["-c", "project_doc_max_bytes=0"];
7294
+ function parse2(line) {
7295
+ try {
7296
+ const e = JSON.parse(line);
7297
+ return typeof e === "object" && e !== null ? e : undefined;
7298
+ } catch {
7299
+ return;
7300
+ }
7301
+ }
7302
+ function unwrapShell(command) {
7303
+ const m = /^\/bin\/(?:ba|z)?sh -l?c (.+)$/s.exec(command);
7304
+ if (!m)
7305
+ return command;
7306
+ const word = m[1];
7307
+ let out = "";
7308
+ for (let i = 0;i < word.length; i++) {
7309
+ const c = word[i];
7310
+ if (c === "'") {
7311
+ const end = word.indexOf("'", i + 1);
7312
+ if (end < 0)
7313
+ return command;
7314
+ out += word.slice(i + 1, end);
7315
+ i = end;
7316
+ } else if (c === "\\" && i + 1 < word.length)
7317
+ out += word[++i];
7318
+ else if (c === '"') {
7319
+ let j = i + 1;
7320
+ for (;j < word.length && word[j] !== '"'; j++) {
7321
+ if (word[j] === "\\" && '"\\$`'.includes(word[j + 1] ?? ""))
7322
+ j++;
7323
+ out += word[j];
7324
+ }
7325
+ if (j >= word.length)
7326
+ return command;
7327
+ i = j;
7328
+ } else if (/\s/.test(c))
7329
+ return command;
7330
+ else
7331
+ out += c;
7332
+ }
7333
+ return out;
7334
+ }
7335
+ function str(v) {
7336
+ return typeof v === "string" ? v : "";
7337
+ }
7338
+ function itemInput(item) {
7339
+ const { id: _id, type: _type, status: _status, ...rest } = item;
7340
+ return rest;
7341
+ }
7342
+ function steps2(events) {
7343
+ const out = [];
7344
+ const started = new Set;
7345
+ for (const e of events) {
7346
+ const item = e?.item;
7347
+ if (!item)
7348
+ continue;
7349
+ if (item.type === "command_execution") {
7350
+ const call = {
7351
+ kind: "tool_call",
7352
+ name: "shell",
7353
+ input: { command: unwrapShell(str(item.command)) }
7354
+ };
7355
+ if (e?.type === "item.started") {
7356
+ started.add(item.id);
7357
+ out.push(call);
7358
+ } else if (e?.type === "item.completed") {
7359
+ if (item.id === undefined || !started.has(item.id))
7360
+ out.push(call);
7361
+ out.push({
7362
+ kind: "tool_result",
7363
+ isError: item.exit_code !== 0,
7364
+ text: str(item.aggregated_output).trimEnd()
7365
+ });
7366
+ }
7367
+ continue;
7368
+ }
7369
+ if (e?.type !== "item.completed")
7370
+ continue;
7371
+ if (item.type === "agent_message") {
7372
+ if (str(item.text).trim() !== "")
7373
+ out.push({ kind: "text", text: str(item.text) });
7374
+ } else if (item.type !== "reasoning" && item.type !== "error")
7375
+ out.push({
7376
+ kind: "tool_call",
7377
+ name: str(item.type),
7378
+ input: itemInput(item)
7379
+ });
7380
+ }
7381
+ return out;
7382
+ }
7383
+ function errorMessage(raw) {
7384
+ try {
7385
+ const inner = JSON.parse(raw)?.error?.message;
7386
+ if (typeof inner === "string")
7387
+ return inner;
7388
+ } catch {}
7389
+ return raw;
7390
+ }
7391
+ function read2(stdout, exited, timeoutMs) {
7392
+ const events = stdout.split(`
7393
+ `).filter((l) => l.trim() !== "").map(parse2);
7394
+ const trace = {
7395
+ steps: steps2(events),
7396
+ final: events.filter((e) => e?.type === "item.completed" && e.item?.type === "agent_message").map((e) => str(e?.item?.text)).at(-1) ?? "",
7397
+ reportedModel: null,
7398
+ version: null,
7399
+ turns: null,
7400
+ cost: null
7401
+ };
7402
+ const thread = events.find((e) => e?.type === "thread.started");
7403
+ if (!thread) {
7404
+ const said = stdout.trim().split(`
7405
+ `)[0]?.slice(0, 300);
7406
+ return {
7407
+ ...trace,
7408
+ ok: false,
7409
+ reason: `codex did not start a thread (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
7410
+ };
7411
+ }
7412
+ const sessionId = str(thread.thread_id);
7413
+ if (sessionId === "")
7414
+ return {
7415
+ ...trace,
7416
+ ok: false,
7417
+ reason: "codex's thread.started line has no thread_id"
7418
+ };
7419
+ if (exited.timedOut)
7420
+ return {
7421
+ ...trace,
7422
+ ok: false,
7423
+ reason: `timed out after ${timeoutMs / 1000}s`,
7424
+ sessionId
7425
+ };
7426
+ const failed = events.find((e) => e?.type === "turn.failed");
7427
+ if (failed) {
7428
+ const message = str(failed.error?.message);
7429
+ return {
7430
+ ...trace,
7431
+ ok: false,
7432
+ reason: `codex turn failed${message ? `: ${errorMessage(message).slice(0, 300)}` : ""}`,
7433
+ sessionId
7434
+ };
7435
+ }
7436
+ if (!events.some((e) => e?.type === "turn.completed"))
7437
+ return {
7438
+ ...trace,
7439
+ ok: false,
7440
+ reason: `codex exited ${exited.code ?? "by signal"} without completing its turn, see stderr.txt`,
7441
+ sessionId
7442
+ };
7443
+ return { ...trace, ok: true, sessionId };
7444
+ }
7445
+ function progress2(line) {
7446
+ const e = parse2(line);
7447
+ const item = e?.item;
7448
+ if (e?.type === "item.started" && item?.type === "command_execution")
7449
+ return { command: unwrapShell(str(item.command)).split(`
7450
+ `)[0] };
7451
+ if (e?.type === "item.completed" && item?.type === "agent_message")
7452
+ return {};
7453
+ return;
7454
+ }
7455
+ function userCodexHome() {
7456
+ return process.env.CODEX_HOME || join3(homedir(), ".codex");
7457
+ }
7458
+ function checkLogin2(opts) {
7459
+ const source = join3(userCodexHome(), "auth.json");
7460
+ if (!existsSync2(source))
7461
+ return {
7462
+ loggedOut: true,
7463
+ reason: `codex is not logged in (no ${source}). Run \`codex login\`, then run eval again`
7464
+ };
7465
+ const link = join3(opts.configDir, "auth.json");
7466
+ let current;
7467
+ try {
7468
+ current = lstatSync(link);
7469
+ } catch {}
7470
+ if (current && !current.isSymbolicLink())
7471
+ return {
7472
+ loggedOut: false,
7473
+ reason: `${link} is a file, not firegoat's link to ${source}: codex may have refreshed its login into it. Move it over ${source} if it is newer, else delete it, then run eval again`
7474
+ };
7475
+ if (current)
7476
+ unlinkSync(link);
7477
+ symlinkSync(source, link);
7478
+ const r = spawnSync2(opts.bin, ["login", "status"], {
7479
+ cwd: opts.cwd,
7480
+ env: opts.env,
7481
+ encoding: "utf8",
7482
+ timeout: 30000
7483
+ });
7484
+ if (r.error)
7485
+ return {
7486
+ loggedOut: false,
7487
+ reason: `could not run ${opts.bin}: ${r.error.message}`
7488
+ };
7489
+ if (r.status === 0)
7490
+ return;
7491
+ return {
7492
+ loggedOut: true,
7493
+ reason: `codex is not logged in (codex login status exit ${r.status}: ${(r.stdout + r.stderr).trim().slice(0, 300)}). Run \`codex login\`, then run eval again`
7494
+ };
7495
+ }
7496
+ function findSession(configDir, sessionId) {
7497
+ const sessions = join3(configDir, "sessions");
7498
+ if (!existsSync2(sessions))
7499
+ return null;
7500
+ const suffix = `-${sessionId}.jsonl`;
7501
+ for (const y of readdirSync2(sessions))
7502
+ for (const m of readdirSync2(join3(sessions, y)))
7503
+ for (const d of readdirSync2(join3(sessions, y, m)))
7504
+ for (const f of readdirSync2(join3(sessions, y, m, d)))
7505
+ if (f.startsWith("rollout-") && f.endsWith(suffix))
7506
+ return join3(sessions, y, m, d, f);
7507
+ return null;
7508
+ }
7509
+ function sessionModel(sessionFile) {
7510
+ let model = null;
7511
+ for (const line of readFileSync(sessionFile, "utf8").split(`
7512
+ `)) {
7513
+ const e = parse2(line);
7514
+ const m = e?.payload?.model;
7515
+ if (e?.type === "turn_context" && typeof m === "string")
7516
+ model = m;
7517
+ }
7518
+ return model;
7519
+ }
7520
+ var codex = {
7521
+ name: "codex-cli",
7522
+ bin: "codex",
7523
+ installHint: "Install Codex CLI (https://github.com/openai/codex) or pass --harness-bin PATH.",
7524
+ configDirName: ".codex",
7525
+ reservedArgs: [
7526
+ [[...FLAGS2, "-C", "--cd"], "firegoat sets it for every run"],
7527
+ [
7528
+ ["-m", "--model"],
7529
+ "choose the model with top-level model: (or eval --model)"
7530
+ ],
7531
+ [["resume", "fork"], "every run is a fresh session"],
7532
+ [["--ephemeral"], "firegoat reads the session file codex saves"]
7533
+ ],
7534
+ uploads: false,
7535
+ args: ({ prompt, model, extraArgs, cwd }) => [
7536
+ "exec",
7537
+ ...FLAGS2,
7538
+ "-C",
7539
+ cwd,
7540
+ ...model === undefined ? [] : ["-m", model],
7541
+ ...NO_PROJECT_DOC,
7542
+ ...extraArgs,
7543
+ "--",
7544
+ prompt
7545
+ ],
7546
+ env: ({ configDir, run }) => ({
7547
+ CODEX_HOME: configDir,
7548
+ ...run === undefined ? {} : homeEnv(run.home)
7549
+ }),
7550
+ authLabel: () => `your codex login (${join3(userCodexHome(), "auth.json")})${process.env.CODEX_API_KEY ? "; CODEX_API_KEY is set and passes through" : ""}`,
7551
+ checkLogin: checkLogin2,
7552
+ progress: progress2,
7553
+ read: read2,
7554
+ sessionsDir: (configDir) => join3(configDir, "sessions"),
7555
+ findSession,
7556
+ sessionModel
7557
+ };
7558
+
7559
+ // src/harnesses/grok.ts
7560
+ import {
7561
+ closeSync,
7562
+ existsSync as existsSync3,
7563
+ lstatSync as lstatSync2,
7564
+ openSync,
7565
+ readdirSync as readdirSync3,
7566
+ readFileSync as readFileSync2,
7567
+ readlinkSync,
7568
+ readSync,
7569
+ statSync,
7570
+ symlinkSync as symlinkSync2,
7571
+ unlinkSync as unlinkSync2
7572
+ } from "node:fs";
7573
+ import { homedir as homedir2 } from "node:os";
7574
+ import { join as join4 } from "node:path";
7575
+ var BIN = join4(homedir2(), ".grok", "bin", "grok");
7576
+ var USER_AUTH = join4(process.env.GROK_HOME || join4(homedir2(), ".grok"), "auth.json");
7577
+ var REFRESH_WINDOW_MS = 5 * 60000;
7578
+ var FLAGS3 = [
7579
+ "--output-format",
7580
+ "streaming-messages-json",
7581
+ "--always-approve",
7582
+ "--no-auto-update"
7583
+ ];
7584
+ function parse3(line) {
7585
+ try {
7586
+ const e = JSON.parse(line);
7587
+ return typeof e === "object" && e !== null ? e : undefined;
7588
+ } catch {
7589
+ return;
7590
+ }
7591
+ }
7592
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7593
+ function content2(e) {
7594
+ const message = e?.message;
7595
+ return Array.isArray(message?.content) ? message.content : [];
7596
+ }
7597
+ var PROMPT_KEYS = ["output_for_prompt", "tool_output_for_prompt", "content"];
7598
+ function promptText(v) {
7599
+ if (typeof v === "string")
7600
+ return v;
7601
+ if (!isObject(v))
7602
+ return;
7603
+ for (const k of PROMPT_KEYS)
7604
+ if (typeof v[k] === "string")
7605
+ return v[k];
7606
+ for (const [k, x] of Object.entries(v)) {
7607
+ if (k === "type")
7608
+ continue;
7609
+ const t = promptText(x);
7610
+ if (t !== undefined)
7611
+ return t;
7612
+ }
7613
+ return;
7614
+ }
7615
+ function toolResult(b) {
7616
+ const raw = b.content;
7617
+ let json;
7618
+ if (typeof raw === "string")
7619
+ try {
7620
+ json = JSON.parse(raw);
7621
+ } catch {}
7622
+ const failed = isObject(json) && (typeof json.exit_code === "number" && json.exit_code !== 0 || json.timed_out === true);
7623
+ const text = isObject(json) ? promptText(json) ?? raw : typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
7624
+ `) : "";
7625
+ return {
7626
+ kind: "tool_result",
7627
+ isError: b.is_error === true || failed,
7628
+ text: text.trimEnd()
7629
+ };
7630
+ }
7631
+ function steps3(events) {
7632
+ const out = [];
7633
+ for (const e of events) {
7634
+ if (e?.type === "assistant")
7635
+ for (const b of content2(e)) {
7636
+ if (b.type === "tool_use")
7637
+ out.push({
7638
+ kind: "tool_call",
7639
+ name: typeof b.name === "string" ? b.name : "",
7640
+ input: isObject(b.input) ? b.input : null
7641
+ });
7642
+ else if (b.type === "text" && typeof b.text === "string")
7643
+ out.push({ kind: "text", text: b.text });
7644
+ }
7645
+ else if (e?.type === "user") {
7646
+ for (const b of content2(e))
7647
+ if (b.type === "tool_result")
7648
+ out.push(toolResult(b));
7649
+ }
7650
+ }
7651
+ return out;
7652
+ }
7653
+ function resultError(result) {
7654
+ const errors = Array.isArray(result.errors) ? result.errors.filter((x) => typeof x === "string").join("; ") : "";
7655
+ const said = errors || result.result || "";
7656
+ return `grok result is an error (subtype=${result.subtype})${said ? `: ${said.slice(0, 300)}` : ""}`;
7657
+ }
7658
+ function read3(stdout, exited, timeoutMs) {
7659
+ const events = stdout.split(`
7660
+ `).filter((l) => l.trim() !== "").map(parse3);
7661
+ const result = events.findLast((e) => e?.type === "result");
7662
+ const trace = {
7663
+ steps: steps3(events),
7664
+ final: typeof result?.result === "string" ? result.result : "",
7665
+ turns: result?.num_turns ?? null,
7666
+ cost: result?.total_cost_usd ?? null
7667
+ };
7668
+ const init = events.find((e) => e?.type === "system" && e.subtype === "init");
7669
+ const reportedModel = typeof init?.model === "string" && init.model !== "unknown" ? init.model : null;
7670
+ const sessionId = typeof init?.session_id === "string" && init.session_id !== "" ? init.session_id : undefined;
7671
+ const fail = (reason) => ({
7672
+ ...trace,
7673
+ reportedModel,
7674
+ version: null,
7675
+ ok: false,
7676
+ reason,
7677
+ ...sessionId === undefined ? {} : { sessionId }
7678
+ });
7679
+ if (exited.timedOut)
7680
+ return fail(`timed out after ${timeoutMs / 1000}s`);
7681
+ if (!init) {
7682
+ const said = stdout.trim().split(`
7683
+ `)[0]?.slice(0, 300);
7684
+ return fail(`grok did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`);
7685
+ }
7686
+ if (result && (result.is_error === true || result.subtype !== "success"))
7687
+ return fail(resultError(result));
7688
+ if (sessionId === undefined)
7689
+ return fail("grok's init line has no session_id");
7690
+ if (!result)
7691
+ return fail(`grok exited ${exited.code ?? "by signal"} without a result, see stderr.txt`);
7692
+ return { ...trace, reportedModel, version: null, ok: true, sessionId };
7693
+ }
7694
+ function progress3(line) {
7695
+ const e = parse3(line);
7696
+ if (e?.type !== "assistant")
7697
+ return;
7698
+ const p = {};
7699
+ for (const b of content2(e)) {
7700
+ const cmd = b.input?.command;
7701
+ if (b.type === "tool_use" && typeof cmd === "string")
7702
+ p.command = cmd.split(`
7703
+ `)[0];
7704
+ }
7705
+ return p;
7706
+ }
7707
+ function isScript(bin) {
7708
+ const fd = openSync(bin, "r");
7709
+ try {
7710
+ const head = Buffer.alloc(2);
7711
+ return readSync(fd, head, 0, 2, 0) === 2 && head.toString() === "#!";
7712
+ } finally {
7713
+ closeSync(fd);
7714
+ }
7715
+ }
7716
+ function loginProblem() {
7717
+ const relogin = "Run `grok login`, then run eval again";
7718
+ if (!existsSync3(USER_AUTH))
7719
+ return {
7720
+ loggedOut: true,
7721
+ reason: `grok is not logged in (no ${USER_AUTH}). ${relogin}. Headless grok needs that login: XAI_API_KEY alone is not enough`
7722
+ };
7723
+ let auth;
7724
+ try {
7725
+ auth = JSON.parse(readFileSync2(USER_AUTH, "utf8"));
7726
+ } catch {
7727
+ return {
7728
+ loggedOut: false,
7729
+ reason: `${USER_AUTH} is not valid JSON. ${relogin}`
7730
+ };
7731
+ }
7732
+ const logins = isObject(auth) ? Object.values(auth).filter((v) => isObject(v) && typeof v.key === "string" && v.key !== "") : [];
7733
+ if (logins.length === 0)
7734
+ return {
7735
+ loggedOut: true,
7736
+ reason: `grok is not logged in (${USER_AUTH} holds no login). ${relogin}`
7737
+ };
7738
+ const expiries = logins.map((l) => l.expires_at);
7739
+ if (expiries.some((x) => x === undefined))
7740
+ return;
7741
+ const times = expiries.map((x) => Date.parse(String(x)));
7742
+ if (times.some(Number.isNaN))
7743
+ return {
7744
+ loggedOut: false,
7745
+ reason: `${USER_AUTH} has an expires_at that is not a date (${expiries.join(", ")}). ${relogin}`
7746
+ };
7747
+ const latest = Math.max(...times);
7748
+ if (latest - Date.now() > REFRESH_WINDOW_MS)
7749
+ return;
7750
+ return {
7751
+ loggedOut: false,
7752
+ reason: `grok's login ${latest < Date.now() ? "expired" : "expires"} at ${new Date(latest).toISOString()}. Run grok once (e.g. \`grok -p hi\`) so it refreshes the login in ${USER_AUTH}, then run eval again: eval's runs must not refresh it themselves`
7753
+ };
7754
+ }
7755
+ function linkAuth(configDir) {
7756
+ const link = join4(configDir, "auth.json");
7757
+ let st;
7758
+ try {
7759
+ st = lstatSync2(link);
7760
+ } catch (e) {
7761
+ if (e.code !== "ENOENT")
7762
+ throw e;
7763
+ }
7764
+ if (st && !st.isSymbolicLink())
7765
+ return {
7766
+ loggedOut: false,
7767
+ reason: `${link} is a file, not a link to ${USER_AUTH}: a grok run refreshed its login there, so it is newer than ${USER_AUTH}. Move it back with \`mv ${link} ${USER_AUTH}\`, then run eval again`
7768
+ };
7769
+ if (st && readlinkSync(link) === USER_AUTH)
7770
+ return;
7771
+ if (st)
7772
+ unlinkSync2(link);
7773
+ symlinkSync2(USER_AUTH, link);
7774
+ return;
7775
+ }
7776
+ function checkLogin3(opts) {
7777
+ if (isScript(opts.bin))
7778
+ return {
7779
+ loggedOut: false,
7780
+ reason: `${opts.bin} is a script, not the grok executable (cmux, for one, puts a bash wrapper named grok on PATH). Pass grok's own binary with --harness-bin, ${BIN} by default`
7781
+ };
7782
+ return loginProblem() ?? linkAuth(opts.configDir);
7783
+ }
7784
+ var grok = {
7785
+ name: "grok-cli",
7786
+ bin: BIN,
7787
+ installHint: "Install grok (curl -fsSL https://x.ai/cli/install.sh | bash) or pass --harness-bin PATH.",
7788
+ configDirName: ".grok",
7789
+ reservedArgs: [
7790
+ [
7791
+ [
7792
+ "-p",
7793
+ "--single",
7794
+ "--prompt-file",
7795
+ "--prompt-json",
7796
+ "--output-format",
7797
+ "--json-schema",
7798
+ "--always-approve",
7799
+ "--permission-mode",
7800
+ "--no-auto-update",
7801
+ "--cwd",
7802
+ "-w",
7803
+ "--worktree"
7804
+ ],
7805
+ "firegoat sets it for every run"
7806
+ ],
7807
+ [
7808
+ ["-m", "--model"],
7809
+ "choose the model with top-level model: (or eval --model)"
7810
+ ],
7811
+ [
7812
+ [
7813
+ "-r",
7814
+ "--resume",
7815
+ "-c",
7816
+ "--continue",
7817
+ "-s",
7818
+ "--session-id",
7819
+ "--fork-session"
7820
+ ],
7821
+ "every run is a fresh session"
7822
+ ]
7823
+ ],
7824
+ uploads: false,
7825
+ args: ({ prompt, model, extraArgs }) => [
7826
+ "-p",
7827
+ prompt,
7828
+ ...FLAGS3,
7829
+ ...model === undefined ? [] : ["-m", model],
7830
+ ...extraArgs
7831
+ ],
7832
+ env: ({ configDir, run }) => ({
7833
+ GROK_HOME: configDir,
7834
+ GROK_DISABLE_AUTOUPDATER: "1",
7835
+ ...run === undefined ? {} : homeEnv(run.home)
7836
+ }),
7837
+ authLabel: () => `your grok login (${USER_AUTH})`,
7838
+ checkLogin: checkLogin3,
7839
+ progress: progress3,
7840
+ read: read3,
7841
+ sessionsDir: (configDir) => join4(configDir, "sessions"),
7842
+ findSession(configDir, sessionId) {
7843
+ const sessions = join4(configDir, "sessions");
7844
+ if (!existsSync3(sessions))
7845
+ return null;
7846
+ for (const d of readdirSync3(sessions)) {
7847
+ const dir = join4(sessions, d, sessionId);
7848
+ if (existsSync3(dir) && statSync(dir).isDirectory())
7849
+ return dir;
7850
+ }
7851
+ return null;
7852
+ }
7853
+ };
7854
+
7855
+ // src/harnesses/index.ts
7856
+ var HARNESSES = new Map([claude, codex, grok].map((h) => [h.name, h]));
7857
+ var DEFAULT_HARNESS = claude.name;
7858
+ function validHarnesses() {
7859
+ return `(valid: ${[...HARNESSES.keys()].join(", ")})`;
7860
+ }
7861
+
7038
7862
  // src/config.ts
7039
7863
  var CONFIG_FILE = "firegoat.yaml";
7040
7864
  var DEFAULT_TIMEOUT = 600;
@@ -7047,7 +7871,7 @@ function fail(path, msg) {
7047
7871
  function isRecord(v) {
7048
7872
  return typeof v === "object" && v !== null && !Array.isArray(v);
7049
7873
  }
7050
- function str(v, path) {
7874
+ function str2(v, path) {
7051
7875
  if (typeof v !== "string" || v.trim() === "")
7052
7876
  fail(path, "must be a non-empty string");
7053
7877
  return v;
@@ -7075,23 +7899,29 @@ function parseConfig(text) {
7075
7899
  fail("(root)", "must be a mapping");
7076
7900
  onlyKeys(raw, "(root)", [
7077
7901
  "runs",
7902
+ "harness",
7078
7903
  "model",
7079
7904
  "build",
7080
7905
  "install",
7081
7906
  "cleanup",
7082
7907
  "vars",
7083
7908
  "env",
7084
- "claude_args",
7909
+ "harness_args",
7085
7910
  "tests"
7086
7911
  ]);
7087
7912
  const runs = posInt(raw.runs, "runs", 1);
7088
- const models = parseModels(raw.model);
7913
+ const harnesses = raw.harness === undefined ? [DEFAULT_HARNESS] : nameList(raw.harness, "harness", "a harness name");
7914
+ harnesses.forEach((h, i) => {
7915
+ if (!HARNESSES.has(h))
7916
+ fail(Array.isArray(raw.harness) ? `harness[${i}]` : "harness", `unknown harness ${JSON.stringify(h)} ${validHarnesses()}`);
7917
+ });
7918
+ const models = raw.model === undefined ? [] : nameList(raw.model, "model", "a model name");
7089
7919
  const build = commands(raw.build, "build", EVAL_PLACEHOLDERS);
7090
7920
  const install = commands(raw.install, "install", RUN_PLACEHOLDERS);
7091
7921
  const cleanup = commands(raw.cleanup, "cleanup", RUN_PLACEHOLDERS);
7092
7922
  const vars = parseVars(raw.vars);
7093
7923
  const env = parseEnv(raw.env, "env");
7094
- const claudeArgs = parseClaudeArgs(raw.claude_args, "claude_args");
7924
+ const harnessArgs = parseHarnessArgs(raw.harness_args, "harness_args");
7095
7925
  if (!Array.isArray(raw.tests) || raw.tests.length === 0)
7096
7926
  fail("tests", "must be a non-empty list");
7097
7927
  const names = new Set;
@@ -7108,9 +7938,9 @@ function parseConfig(text) {
7108
7938
  "env",
7109
7939
  "install",
7110
7940
  "cleanup",
7111
- "claude_args"
7941
+ "harness_args"
7112
7942
  ]);
7113
- const name = str(t.name, `${p}.name`);
7943
+ const name = str2(t.name, `${p}.name`);
7114
7944
  if (!NAME.test(name))
7115
7945
  fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
7116
7946
  if (names.has(name))
@@ -7125,27 +7955,28 @@ function parseConfig(text) {
7125
7955
  if (!isRecord(c))
7126
7956
  fail(cp, "must be a mapping");
7127
7957
  onlyKeys(c, cp, ["run"]);
7128
- const run = str(c.run, `${cp}.run`);
7958
+ const run = str2(c.run, `${cp}.run`);
7129
7959
  checkPlaceholders(run, `${cp}.run`, RUN_PLACEHOLDERS);
7130
7960
  checks.push({ run });
7131
7961
  });
7132
7962
  return {
7133
7963
  name,
7134
- prompt: str(t.prompt, `${p}.prompt`),
7135
- end_goal: str(t.end_goal, `${p}.end_goal`),
7964
+ prompt: str2(t.prompt, `${p}.prompt`),
7965
+ end_goal: str2(t.end_goal, `${p}.end_goal`),
7136
7966
  checks,
7137
7967
  timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
7138
7968
  env: { ...env, ...parseEnv(t.env, `${p}.env`) },
7139
7969
  install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
7140
7970
  cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
7141
- claudeArgs: [
7142
- ...claudeArgs,
7143
- ...parseClaudeArgs(t.claude_args, `${p}.claude_args`)
7971
+ harnessArgs: [
7972
+ ...harnessArgs,
7973
+ ...parseHarnessArgs(t.harness_args, `${p}.harness_args`)
7144
7974
  ]
7145
7975
  };
7146
7976
  });
7147
7977
  return {
7148
7978
  runs,
7979
+ harnesses,
7149
7980
  models,
7150
7981
  build,
7151
7982
  install,
@@ -7164,63 +7995,45 @@ function commands(raw, path, allowed) {
7164
7995
  const list = typeof raw === "string" ? [raw] : raw;
7165
7996
  return list.map((c, i) => {
7166
7997
  const p = typeof raw === "string" ? path : `${path}[${i}]`;
7167
- const cmd = str(c, p);
7998
+ const cmd = str2(c, p);
7168
7999
  checkPlaceholders(cmd, p, allowed);
7169
8000
  return cmd;
7170
8001
  });
7171
8002
  }
7172
- function parseModels(raw) {
7173
- if (raw === undefined)
7174
- return [];
8003
+ function nameList(raw, key, what) {
7175
8004
  if (typeof raw === "string")
7176
- return [str(raw, "model")];
8005
+ return [str2(raw, key)];
7177
8006
  if (!Array.isArray(raw) || raw.length === 0)
7178
- fail("model", "must be a model name or a non-empty list of them");
8007
+ fail(key, `must be ${what} or a non-empty list of them`);
7179
8008
  const seen = new Set;
7180
8009
  return raw.map((m, i) => {
7181
- const name = str(m, `model[${i}]`);
8010
+ const name = str2(m, `${key}[${i}]`);
7182
8011
  if (seen.has(name))
7183
- fail(`model[${i}]`, `duplicate model ${JSON.stringify(name)}`);
8012
+ fail(`${key}[${i}]`, `duplicate ${key} ${JSON.stringify(name)}`);
7184
8013
  seen.add(name);
7185
8014
  return name;
7186
8015
  });
7187
8016
  }
7188
- var OWNED_FLAGS = [
7189
- [
7190
- [
7191
- "-p",
7192
- "--print",
7193
- "--output-format",
7194
- "--verbose",
7195
- "--setting-sources",
7196
- "--strict-mcp-config",
7197
- "--disable-slash-commands",
7198
- "--dangerously-skip-permissions"
7199
- ],
7200
- "firegoat sets it for every run"
7201
- ],
7202
- [["--model"], "choose the model with top-level model: (or eval --model)"],
7203
- [
7204
- ["--session-id", "--resume", "-r", "--continue", "-c"],
7205
- "every run is a fresh session"
7206
- ]
7207
- ];
7208
- function parseClaudeArgs(raw, path) {
8017
+ function parseHarnessArgs(raw, path) {
7209
8018
  if (raw === undefined)
7210
8019
  return [];
7211
8020
  if (!Array.isArray(raw))
7212
- fail(path, "must be a list of claude arguments, one per entry, e.g. [--chrome]");
8021
+ fail(path, "must be a list of harness arguments, one per entry, e.g. [--chrome]");
7213
8022
  return raw.map((a, i) => {
7214
8023
  const p = `${path}[${i}]`;
7215
- const arg = str(a, p);
7216
- for (const [flags, why] of OWNED_FLAGS)
7217
- for (const f of flags)
7218
- if (arg === f || arg.startsWith(`${f}=`))
7219
- fail(p, `${f} is not allowed: ${why}`);
8024
+ const arg = str2(a, p);
7220
8025
  checkPlaceholders(arg, p, RUN_PLACEHOLDERS);
7221
- return arg;
8026
+ return { arg, path: p };
7222
8027
  });
7223
8028
  }
8029
+ function checkHarnessArgs(tests, harness) {
8030
+ for (const t of tests)
8031
+ for (const { arg, path } of t.harnessArgs)
8032
+ for (const [flags, why] of harness.reservedArgs)
8033
+ for (const f of flags)
8034
+ if (arg === f || arg.startsWith(`${f}=`))
8035
+ fail(path, `${f} is not allowed for ${harness.name}: ${why}`);
8036
+ }
7224
8037
  var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
7225
8038
  function parseVars(raw) {
7226
8039
  if (raw === undefined)
@@ -7240,7 +8053,10 @@ function parseVars(raw) {
7240
8053
  var RESERVED_ENV = new Set([
7241
8054
  "PATH",
7242
8055
  "CLAUDE_CONFIG_DIR",
7243
- "CLAUDE_SECURESTORAGE_CONFIG_DIR"
8056
+ "CLAUDE_SECURESTORAGE_CONFIG_DIR",
8057
+ "CODEX_HOME",
8058
+ "GROK_HOME",
8059
+ "GROK_DISABLE_AUTOUPDATER"
7244
8060
  ]);
7245
8061
  var HOME_ENV = new Set([
7246
8062
  "HOME",
@@ -7270,7 +8086,7 @@ function parseEnv(raw, path) {
7270
8086
  if (HOME_ENV.has(k))
7271
8087
  fail(p, "Reserved: every run gets its own home ($FIREGOAT_HOME)");
7272
8088
  if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
7273
- fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
8089
+ fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, CODEX_HOME, GROK_HOME, GROK_DISABLE_AUTOUPDATER, FIREGOAT_*)");
7274
8090
  if (typeof v !== "string")
7275
8091
  fail(p, "must be a string (quote numbers and booleans)");
7276
8092
  checkPlaceholders(v, p, RUN_PLACEHOLDERS);
@@ -7324,9 +8140,9 @@ function render(text, vars) {
7324
8140
  }
7325
8141
  function loadConfig(dir) {
7326
8142
  const file = `${dir}/${CONFIG_FILE}`;
7327
- if (!existsSync(file))
8143
+ if (!existsSync4(file))
7328
8144
  throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
7329
- return parseConfig(readFileSync(file, "utf8"));
8145
+ return parseConfig(readFileSync3(file, "utf8"));
7330
8146
  }
7331
8147
 
7332
8148
  // src/eval.ts
@@ -7334,27 +8150,71 @@ import { randomBytes as randomBytes2 } from "node:crypto";
7334
8150
  import {
7335
8151
  appendFileSync,
7336
8152
  createWriteStream as createWriteStream2,
7337
- mkdirSync as mkdirSync4,
8153
+ mkdirSync as mkdirSync5,
7338
8154
  writeFileSync as writeFileSync4
7339
8155
  } from "node:fs";
7340
- import { basename as basename3, delimiter as delimiter2, join as join7 } from "node:path";
8156
+ import { basename as basename3, delimiter as delimiter2, join as join10 } from "node:path";
8157
+
8158
+ // src/agent-dirs.ts
8159
+ import { createHash, randomInt } from "node:crypto";
8160
+ import { mkdirSync as mkdirSync2, realpathSync, symlinkSync as symlinkSync3 } from "node:fs";
8161
+ import { homedir as homedir3 } from "node:os";
8162
+ import { join as join5 } from "node:path";
8163
+ var ID_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
8164
+ var ID_LENGTH = 8;
8165
+ var MAX_TRIES = 10;
8166
+ function projectAgentDir(projectRoot) {
8167
+ const hash = createHash("sha256").update(realpathSync(projectRoot)).digest("hex").slice(0, 12);
8168
+ return join5(firegoatHome(), "runs", hash);
8169
+ }
8170
+ function firegoatHome() {
8171
+ return join5(realpathSync(homedir3()), ".firegoat");
8172
+ }
8173
+ function randomId() {
8174
+ let id = "";
8175
+ for (let i = 0;i < ID_LENGTH; i++)
8176
+ id += ID_CHARS[randomInt(ID_CHARS.length)];
8177
+ return id;
8178
+ }
8179
+ function ensureProjectAgentDir(projectRoot) {
8180
+ mkdirSync2(firegoatHome(), { recursive: true, mode: 448 });
8181
+ const dir = projectAgentDir(projectRoot);
8182
+ mkdirSync2(dir, { recursive: true });
8183
+ return dir;
8184
+ }
8185
+ function makeAgentDir(projectDir) {
8186
+ for (let i = 0;i < MAX_TRIES; i++) {
8187
+ const dir = join5(projectDir, randomId());
8188
+ try {
8189
+ mkdirSync2(dir);
8190
+ return dir;
8191
+ } catch (e) {
8192
+ if (e.code !== "EEXIST")
8193
+ throw e;
8194
+ }
8195
+ }
8196
+ throw new Error(`Could not create a new run folder in ${projectDir}: ${MAX_TRIES} random names were taken`);
8197
+ }
8198
+ function linkDir(target, link) {
8199
+ symlinkSync3(target, link, "dir");
8200
+ }
7341
8201
 
7342
8202
  // src/credentials.ts
7343
8203
  import { randomBytes } from "node:crypto";
7344
8204
  import {
7345
8205
  chmodSync,
7346
- closeSync,
8206
+ closeSync as closeSync2,
7347
8207
  fsyncSync,
7348
- lstatSync,
7349
- mkdirSync,
7350
- openSync,
7351
- readFileSync as readFileSync2,
8208
+ lstatSync as lstatSync3,
8209
+ mkdirSync as mkdirSync3,
8210
+ openSync as openSync2,
8211
+ readFileSync as readFileSync4,
7352
8212
  renameSync,
7353
- unlinkSync,
8213
+ unlinkSync as unlinkSync3,
7354
8214
  writeSync
7355
8215
  } from "node:fs";
7356
- import { homedir } from "node:os";
7357
- import { join } from "node:path";
8216
+ import { homedir as homedir4 } from "node:os";
8217
+ import { join as join6 } from "node:path";
7358
8218
 
7359
8219
  // src/firegoat-api.ts
7360
8220
  var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
@@ -7506,10 +8366,10 @@ async function whoami(base, key) {
7506
8366
  // src/credentials.ts
7507
8367
  var KEY = /^fire_[\x21-\x7e]+$/;
7508
8368
  function credentialsDir() {
7509
- return join(homedir(), ".firegoat");
8369
+ return join6(homedir4(), ".firegoat");
7510
8370
  }
7511
8371
  function credentialsPath() {
7512
- return join(credentialsDir(), "credentials.json");
8372
+ return join6(credentialsDir(), "credentials.json");
7513
8373
  }
7514
8374
  function uid() {
7515
8375
  if (!process.getuid)
@@ -7518,7 +8378,7 @@ function uid() {
7518
8378
  }
7519
8379
  function lstatOrNull(p) {
7520
8380
  try {
7521
- return lstatSync(p);
8381
+ return lstatSync3(p);
7522
8382
  } catch (e) {
7523
8383
  if (e.code === "ENOENT")
7524
8384
  return null;
@@ -7548,7 +8408,7 @@ function loadCredentials() {
7548
8408
  throw new ConfigError(`${path} is readable by others (mode ${(st.mode & 511).toString(8)}): chmod 600 it or run firegoat login again`);
7549
8409
  let c;
7550
8410
  try {
7551
- c = JSON.parse(readFileSync2(path, "utf8"));
8411
+ c = JSON.parse(readFileSync4(path, "utf8"));
7552
8412
  } catch {
7553
8413
  throw new ConfigError(`${path} is not valid JSON: run firegoat logout, then firegoat login`);
7554
8414
  }
@@ -7572,20 +8432,20 @@ function saveCredentials(c) {
7572
8432
  if (st)
7573
8433
  assertOwned(dir, st, "dir");
7574
8434
  else
7575
- mkdirSync(dir, { mode: 448 });
8435
+ mkdirSync3(dir, { mode: 448 });
7576
8436
  chmodSync(dir, 448);
7577
8437
  const tmp = `${credentialsPath()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
7578
- const fd = openSync(tmp, "wx", 384);
8438
+ const fd = openSync2(tmp, "wx", 384);
7579
8439
  try {
7580
8440
  writeSync(fd, `${JSON.stringify(c, null, 2)}
7581
8441
  `);
7582
8442
  fsyncSync(fd);
7583
8443
  } catch (e) {
7584
- closeSync(fd);
7585
- unlinkSync(tmp);
8444
+ closeSync2(fd);
8445
+ unlinkSync3(tmp);
7586
8446
  throw e;
7587
8447
  }
7588
- closeSync(fd);
8448
+ closeSync2(fd);
7589
8449
  renameSync(tmp, credentialsPath());
7590
8450
  }
7591
8451
  function removeCredentials() {
@@ -7606,7 +8466,7 @@ function removeCredentials() {
7606
8466
  if (!(e instanceof ConfigError))
7607
8467
  throw e;
7608
8468
  }
7609
- unlinkSync(path);
8469
+ unlinkSync3(path);
7610
8470
  return { saved };
7611
8471
  }
7612
8472
  function resolveKey(base) {
@@ -7628,15 +8488,15 @@ function resolveKey(base) {
7628
8488
  var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
7629
8489
 
7630
8490
  // src/firegoat-dir.ts
7631
- import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
7632
- import { join as join2 } from "node:path";
8491
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
8492
+ import { join as join7 } from "node:path";
7633
8493
  var GITIGNORE = `*
7634
8494
  `;
7635
8495
  function ensureFiregoatDir(root) {
7636
- const dir = join2(root, ".firegoat");
7637
- mkdirSync2(dir, { recursive: true });
8496
+ const dir = join7(root, ".firegoat");
8497
+ mkdirSync4(dir, { recursive: true });
7638
8498
  try {
7639
- writeFileSync(join2(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
8499
+ writeFileSync2(join7(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
7640
8500
  } catch (e) {
7641
8501
  if (e.code !== "EEXIST")
7642
8502
  throw e;
@@ -7645,17 +8505,48 @@ function ensureFiregoatDir(root) {
7645
8505
  }
7646
8506
 
7647
8507
  // src/git.ts
7648
- import { spawnSync } from "node:child_process";
8508
+ import { spawnSync as spawnSync3 } from "node:child_process";
7649
8509
  import { basename, dirname } from "node:path";
7650
8510
 
7651
8511
  // src/process.ts
7652
- import { spawn } from "node:child_process";
8512
+ import { execFileSync, spawn } from "node:child_process";
7653
8513
  var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
7654
8514
  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)$/;
7655
8515
  var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
7656
8516
  function hostEnv() {
7657
8517
  return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
7658
8518
  }
8519
+ function processTree(pid) {
8520
+ const children = new Map;
8521
+ const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
8522
+ encoding: "utf8"
8523
+ });
8524
+ for (const line of ps.split(`
8525
+ `)) {
8526
+ const [child, parent] = line.trim().split(/\s+/).map(Number);
8527
+ if (!child || parent === undefined)
8528
+ continue;
8529
+ children.set(parent, [...children.get(parent) ?? [], child]);
8530
+ }
8531
+ const tree = [pid];
8532
+ for (let i = 0;i < tree.length; i++)
8533
+ tree.push(...children.get(tree[i]) ?? []);
8534
+ return tree;
8535
+ }
8536
+ function killTree(pid) {
8537
+ let tree = [pid];
8538
+ let problem;
8539
+ try {
8540
+ tree = processTree(pid);
8541
+ } catch (e) {
8542
+ problem = e.message;
8543
+ }
8544
+ for (const p of tree)
8545
+ try {
8546
+ process.kill(p, "SIGKILL");
8547
+ } catch {}
8548
+ return problem;
8549
+ }
7659
8550
  function run(opts) {
7660
8551
  return new Promise((resolve) => {
7661
8552
  const child = spawn(opts.cmd, opts.args, {
@@ -7667,7 +8558,12 @@ function run(opts) {
7667
8558
  let settled = false;
7668
8559
  const timer = setTimeout(() => {
7669
8560
  timedOut = true;
7670
- child.kill("SIGKILL");
8561
+ if (child.pid === undefined)
8562
+ return;
8563
+ const problem = killTree(child.pid);
8564
+ if (problem)
8565
+ opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
8566
+ `));
7671
8567
  }, opts.timeoutMs);
7672
8568
  const done = (r) => {
7673
8569
  if (settled)
@@ -7714,7 +8610,7 @@ async function runShell(opts) {
7714
8610
 
7715
8611
  // src/git.ts
7716
8612
  function git(dir, args) {
7717
- const r = spawnSync("git", args, {
8613
+ const r = spawnSync3("git", args, {
7718
8614
  cwd: dir,
7719
8615
  env: hostEnv(),
7720
8616
  encoding: "utf8"
@@ -7724,7 +8620,7 @@ function git(dir, args) {
7724
8620
  return { code: r.status ?? -1, stdout: r.stdout.trim() };
7725
8621
  }
7726
8622
  function gitRepo(dir) {
7727
- const r = spawnSync("git", [
8623
+ const r = spawnSync3("git", [
7728
8624
  "rev-parse",
7729
8625
  "--path-format=absolute",
7730
8626
  "--git-dir",
@@ -7807,213 +8703,11 @@ async function runAgent(harness, opts) {
7807
8703
  return harness.read(stdout, exited, opts.timeoutMs);
7808
8704
  }
7809
8705
 
7810
- // src/harnesses/claude.ts
7811
- import { spawnSync as spawnSync2 } from "node:child_process";
7812
- import { existsSync as existsSync2, readdirSync } from "node:fs";
7813
- import { join as join3 } from "node:path";
7814
- var FLAGS = [
7815
- "--output-format",
7816
- "stream-json",
7817
- "--verbose",
7818
- "--dangerously-skip-permissions",
7819
- "--strict-mcp-config",
7820
- "--setting-sources",
7821
- "",
7822
- "--disable-slash-commands"
7823
- ];
7824
- function parse(line) {
7825
- try {
7826
- const e = JSON.parse(line);
7827
- return typeof e === "object" && e !== null ? e : undefined;
7828
- } catch {
7829
- return;
7830
- }
7831
- }
7832
- function content(e) {
7833
- const message = e?.message;
7834
- return Array.isArray(message?.content) ? message.content : [];
7835
- }
7836
- function resultText(content) {
7837
- if (typeof content === "string")
7838
- return content;
7839
- if (Array.isArray(content))
7840
- return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
7841
- `);
7842
- return "";
7843
- }
7844
- function steps(events) {
7845
- const out = [];
7846
- for (const e of events) {
7847
- if (e?.type === "assistant")
7848
- for (const b of content(e)) {
7849
- if (b.type === "tool_use")
7850
- out.push({
7851
- kind: "tool_call",
7852
- name: typeof b.name === "string" ? b.name : "",
7853
- input: typeof b.input === "object" && b.input !== null ? b.input : null
7854
- });
7855
- else if (b.type === "text" && typeof b.text === "string")
7856
- out.push({ kind: "text", text: b.text });
7857
- }
7858
- else if (e?.type === "user") {
7859
- for (const b of content(e))
7860
- if (b.type === "tool_result")
7861
- out.push({
7862
- kind: "tool_result",
7863
- isError: b.is_error === true,
7864
- text: resultText(b.content).trimEnd()
7865
- });
7866
- }
7867
- }
7868
- return out;
7869
- }
7870
- function read(stdout, exited, timeoutMs) {
7871
- const events = stdout.split(`
7872
- `).filter((l) => l.trim() !== "").map(parse);
7873
- let final = "";
7874
- for (const e of events)
7875
- if (e?.type === "result" && typeof e.result === "string")
7876
- final = e.result;
7877
- const result = events.findLast((e) => e?.type === "result");
7878
- const trace = {
7879
- steps: steps(events),
7880
- final,
7881
- turns: result?.num_turns ?? null,
7882
- cost: result?.total_cost_usd ?? null
7883
- };
7884
- const init = events.find((e) => e?.type === "system" && e.subtype === "init");
7885
- if (!init) {
7886
- const said = stdout.trim().split(`
7887
- `)[0]?.slice(0, 300);
7888
- return {
7889
- ...trace,
7890
- reportedModel: null,
7891
- version: null,
7892
- ok: false,
7893
- reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
7894
- };
7895
- }
7896
- const reportedModel = typeof init.model === "string" ? init.model : null;
7897
- const version = typeof init.claude_code_version === "string" ? init.claude_code_version : null;
7898
- if (typeof init.session_id !== "string" || init.session_id === "")
7899
- return {
7900
- ...trace,
7901
- reportedModel,
7902
- version,
7903
- ok: false,
7904
- reason: "claude's init line has no session_id"
7905
- };
7906
- const sessionId = init.session_id;
7907
- if (exited.timedOut)
7908
- return {
7909
- ...trace,
7910
- reportedModel,
7911
- version,
7912
- ok: false,
7913
- reason: `timed out after ${timeoutMs / 1000}s`,
7914
- sessionId
7915
- };
7916
- if (!result)
7917
- return {
7918
- ...trace,
7919
- reportedModel,
7920
- version,
7921
- ok: false,
7922
- reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
7923
- sessionId
7924
- };
7925
- if (result.is_error === true || result.subtype !== "success")
7926
- return {
7927
- ...trace,
7928
- reportedModel,
7929
- version,
7930
- ok: false,
7931
- reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
7932
- sessionId
7933
- };
7934
- return { ...trace, reportedModel, version, ok: true, sessionId };
7935
- }
7936
- function progress(line) {
7937
- const e = parse(line);
7938
- if (e?.type !== "assistant")
7939
- return;
7940
- const p = {};
7941
- for (const b of content(e)) {
7942
- const cmd = b.input?.command;
7943
- if (b.type === "tool_use" && typeof cmd === "string")
7944
- p.command = cmd.split(`
7945
- `)[0];
7946
- }
7947
- return p;
7948
- }
7949
- function checkLogin(opts) {
7950
- const r = spawnSync2(opts.bin, ["auth", "status"], {
7951
- cwd: opts.cwd,
7952
- env: opts.env,
7953
- encoding: "utf8",
7954
- timeout: 30000
7955
- });
7956
- if (r.error)
7957
- return {
7958
- loggedOut: false,
7959
- reason: `could not run ${opts.bin}: ${r.error.message}`
7960
- };
7961
- let status;
7962
- try {
7963
- status = JSON.parse(r.stdout);
7964
- } catch {}
7965
- if (status?.loggedIn === true)
7966
- return;
7967
- if (status?.loggedIn === false)
7968
- return {
7969
- loggedOut: true,
7970
- reason: "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again"
7971
- };
7972
- return {
7973
- loggedOut: false,
7974
- reason: `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`
7975
- };
7976
- }
7977
- var claude = {
7978
- name: "claude",
7979
- bin: "claude",
7980
- installHint: "Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH.",
7981
- configDirName: ".claude",
7982
- args: ({ prompt, model, extraArgs }) => [
7983
- "-p",
7984
- prompt,
7985
- ...FLAGS,
7986
- ...model === undefined ? [] : ["--model", model],
7987
- ...extraArgs
7988
- ],
7989
- env: ({ configDir, homeEnvFile }) => ({
7990
- CLAUDE_CONFIG_DIR: configDir,
7991
- CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
7992
- ...homeEnvFile === undefined ? {} : { CLAUDE_ENV_FILE: homeEnvFile }
7993
- }),
7994
- authLabel: () => process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login",
7995
- checkLogin,
7996
- progress,
7997
- read,
7998
- sessionsDir: (configDir) => join3(configDir, "projects"),
7999
- findSession(configDir, sessionId) {
8000
- const projects = join3(configDir, "projects");
8001
- if (!existsSync2(projects))
8002
- return null;
8003
- for (const d of readdirSync(projects)) {
8004
- const f = join3(projects, d, `${sessionId}.jsonl`);
8005
- if (existsSync2(f))
8006
- return f;
8007
- }
8008
- return null;
8009
- }
8010
- };
8011
-
8012
8706
  // src/help.ts
8013
8707
  var EXIT_CODES = `Exit codes:
8014
8708
  0 Every run's script checks passed
8015
8709
  1 A script check failed
8016
- 2 At least one run was not graded (claude missing or logged out, crash, timeout,
8710
+ 2 At least one run was not graded (the harness missing or logged out, crash, timeout,
8017
8711
  build failed); wins over 1
8018
8712
  3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
8019
8713
  firegoat.ai answered 401/403 for the upload key); any other failure of
@@ -8067,10 +8761,18 @@ firegoat.yaml, committed.
8067
8761
 
8068
8762
  firegoat.yaml reference:
8069
8763
  runs: 1 Runs per test
8764
+ harness: <name> | [<name>, ...]
8765
+ The agent CLI: claude-code (default), codex-cli
8766
+ or grok-cli.
8767
+ A list runs every test runs times per harness;
8768
+ eval --harness overrides
8070
8769
  model: <name> | [<name>, ...]
8071
- claude --model; a list runs every test runs times
8072
- per model (n counts 1..runs x models). Unset: no
8073
- --model, claude's default. eval --model overrides
8770
+ The harness's --model (-m for codex, grok); a list
8771
+ runs
8772
+ every test runs times per model, on each harness
8773
+ (n counts 1..runs x harnesses x models, harness by
8774
+ harness, then model). Unset: no --model, the
8775
+ harness's default. eval --model overrides
8074
8776
  build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
8075
8777
  $FIREGOAT_OUT is first on the agent's PATH
8076
8778
  install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
@@ -8090,7 +8792,8 @@ firegoat.yaml reference:
8090
8792
  $FIREGOAT_HOME (no other $ expansion: $HOME stays
8091
8793
  literal). The build step gets only the entries that
8092
8794
  use only {{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
8093
- PATH, CLAUDE_CONFIG_DIR, FIREGOAT_* and
8795
+ PATH, CLAUDE_CONFIG_DIR, CODEX_HOME, GROK_HOME,
8796
+ GROK_DISABLE_AUTOUPDATER, FIREGOAT_* and
8094
8797
  ANTHROPIC_MODEL (use model:) are reserved, and so
8095
8798
  are HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME,
8096
8799
  XDG_DATA_HOME, XDG_STATE_HOME and CLAUDE_ENV_FILE:
@@ -8098,20 +8801,33 @@ firegoat.yaml reference:
8098
8801
  tool that keeps state in ~ starts fresh in each
8099
8802
  run. Only a tool that ignores HOME needs its own
8100
8803
  var, e.g. MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
8101
- claude_args: [<arg>, ...] Extra claude arguments, after firegoat's own flags
8102
- and --model. Each entry is one argument, passed
8103
- as-is (no shell splitting): [--chrome] (Claude in
8104
- Chrome, for tests that need a browser),
8105
- [--plugin-dir, ./plugins/x], --mcp-config, --agents,
8106
- --append-system-prompt. Entries expand like env:
8107
- values ($FIREGOAT_WORKDIR, $FIREGOAT_HOME,
8108
- $FIREGOAT_OUT, {{vars.KEY}}, {{run.*}}); run.json
8109
- claudeArgs has the result. Flags firegoat sets are
8110
- errors, also as --flag=value: -p, --print,
8804
+ harness_args: [<arg>, ...] Extra harness arguments, after firegoat's own flags
8805
+ and --model (for codex, before the prompt), to
8806
+ every selected harness. Each entry is one
8807
+ argument, passed as-is (no shell splitting):
8808
+ claude: [--chrome] (Claude in Chrome, for tests
8809
+ that need a browser), [--plugin-dir, ./plugins/x],
8810
+ --mcp-config, --agents, --append-system-prompt;
8811
+ codex: [-c, key=value], --enable, --add-dir.
8812
+ Entries expand like env: values
8813
+ ($FIREGOAT_WORKDIR, $FIREGOAT_HOME, $FIREGOAT_OUT,
8814
+ {{vars.KEY}}, {{run.*}}); run.json harnessArgs has
8815
+ the result. Flags firegoat sets are errors, also
8816
+ as --flag=value. claude-code: -p, --print,
8111
8817
  --output-format, --verbose, --setting-sources,
8112
8818
  --strict-mcp-config, --disable-slash-commands,
8113
8819
  --dangerously-skip-permissions, --model (use
8114
- model:), --session-id, --resume, -r, --continue, -c
8820
+ model:), --session-id, --resume, -r, --continue,
8821
+ -c. codex-cli: --json, --skip-git-repo-check,
8822
+ --dangerously-bypass-approvals-and-sandbox, -C,
8823
+ --cd, -m, --model (use model:), resume, fork,
8824
+ --ephemeral. grok-cli: -p, --single,
8825
+ --prompt-file, --prompt-json, --output-format,
8826
+ --json-schema, --always-approve,
8827
+ --permission-mode, --no-auto-update, --cwd, -w,
8828
+ --worktree, -m, --model (use model:), -r,
8829
+ --resume, -c, --continue, -s, --session-id,
8830
+ --fork-session
8115
8831
  tests:
8116
8832
  - name: quickstart Letters, digits, '.', '_', '-'; unique
8117
8833
  prompt: ... What a user would ask the agent; one concrete outcome
@@ -8123,7 +8839,7 @@ firegoat.yaml reference:
8123
8839
  env: {NAME: value} Optional, on top of the top-level env:
8124
8840
  install: <sh> | [...] Optional, runs after the top-level install
8125
8841
  cleanup: <sh> | [...] Optional, runs before the top-level cleanup
8126
- claude_args: [...] Optional, after the top-level claude_args
8842
+ harness_args: [...] Optional, after the top-level harness_args
8127
8843
 
8128
8844
  Step order: build once per eval; then per run: install (top-level, then the
8129
8845
  test's), the agent, checks, cleanup (the test's, then top-level).
@@ -8140,11 +8856,15 @@ firegoat.yaml reference:
8140
8856
  for any value.
8141
8857
  The build, install and cleanup commands and the checks get the vars as env
8142
8858
  vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
8143
- get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME).
8859
+ get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME),
8860
+ $FIREGOAT_RUN_DIR (the run's .firegoat/runs/<runId>/<test>/<n>/, where its logs
8861
+ go) and $FIREGOAT_REPO (the folder holding firegoat.yaml). The agent gets
8862
+ neither: keep values it must not find in $FIREGOAT_RUN_DIR, e.g. install:
8863
+ echo "$EXPECTED" > "$FIREGOAT_RUN_DIR/expected", and a check reads it back.
8144
8864
  Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
8145
8865
  transcript, greppable line by line (format: eval --help, Transcript). So
8146
8866
  '^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
8147
- $FIREGOAT_SESSION (claude's full session file, JSON lines).
8867
+ $FIREGOAT_SESSION (the harness's full session file, JSON lines).
8148
8868
 
8149
8869
  The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
8150
8870
  (npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
@@ -8155,7 +8875,7 @@ firegoat.yaml reference:
8155
8875
  so the agent's commands see them too.
8156
8876
 
8157
8877
  Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
8158
- and pass it with claude_args. Only the configs you pass load: --strict-mcp-config
8878
+ and pass it with harness_args (claude-code). Only the configs you pass load: --strict-mcp-config
8159
8879
  is always on. An MCP server inherits claude's env, which keeps your real HOME
8160
8880
  (claude's login is there), so set the server's HOME to the run's home, or it
8161
8881
  writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
@@ -8166,7 +8886,7 @@ relative paths in it are from the run's folder, claude's cwd:
8166
8886
  "env": {"HOME": "\${FIREGOAT_HOME}"}}}}
8167
8887
  firegoat.yaml:
8168
8888
  build: cp -R fixtures/. "$FIREGOAT_OUT"
8169
- claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
8889
+ harness_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
8170
8890
 
8171
8891
  Example:
8172
8892
  build: npm pack --pack-destination "$FIREGOAT_OUT"
@@ -8190,7 +8910,7 @@ Example:
8190
8910
  Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put
8191
8911
  it in .env as ACME_API_KEY.
8192
8912
  end_goal: .env holds a key copied from the dashboard, not a made-up one.
8193
- claude_args: [--chrome]
8913
+ harness_args: [--chrome]
8194
8914
 
8195
8915
  init also says that firegoat sends anonymous usage stats; FIREGOAT_TELEMETRY=0
8196
8916
  (or DO_NOT_TRACK=1) turns them off. Fields: firegoat --help, Telemetry.
@@ -8235,11 +8955,17 @@ Exit codes: 0 logged out (or was not logged in), 3 the file or ~/.firegoat is a
8235
8955
  symlink, not a regular file/directory, or someone else's (left alone).`;
8236
8956
  var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--parallel N]
8237
8957
  [--var KEY=value].. [--json] [--no-upload] [--dry-run]
8238
- [--claude-bin PATH]
8239
-
8240
- Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
8241
- in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
8242
- outputs on PATH. Then the test's checks run in that folder.
8958
+ [--harness NAME].. [--harness-bin PATH]
8959
+
8960
+ Runs each test in firegoat.yaml (in the current folder): your agent harness
8961
+ (claude-code by default, codex-cli or grok-cli; harness: in firegoat.yaml) gets the prompt
8962
+ in a fresh, empty folder outside the repo, ~/.firegoat/runs/<hash>/<random>/work,
8963
+ with build:'s outputs on PATH. Then the test's checks run in that folder. <hash>
8964
+ is a hash of the repo's path (a linked worktree uses its main checkout's) and
8965
+ <random> is new for each run, so the agent's paths name neither the project nor
8966
+ the test, and git there sees no repo. The run's logs stay in the repo, in
8967
+ .firegoat/runs/<runId>/<test>/<n>/, whose work and home are symlinks to the
8968
+ run's folders.
8243
8969
 
8244
8970
  Step order: build: once, in this folder; then per run, in its folder: install:
8245
8971
  (top-level, then the test's), the agent, checks, cleanup: (the test's, then
@@ -8248,16 +8974,39 @@ a claude error or a timeout; a failing cleanup step prints a warning and never
8248
8974
  changes the run's status or the exit code.
8249
8975
 
8250
8976
  claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
8251
- with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
8252
- plugins, skills or MCP servers (claude_args: in firegoat.yaml adds claude flags,
8253
- e.g. --chrome or --plugin-dir). claude saves its session there. There is no
8254
- sandbox: the agent can reach the rest of your machine, including the repo.
8255
-
8256
- Every run gets its own HOME, .firegoat/runs/<runId>/<test>/<n>/home (with
8257
- XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
8977
+ with a fresh config dir, ~/.firegoat/runs/<hash>/.claude: none of your settings,
8978
+ CLAUDE.md, plugins, skills or MCP servers (harness_args: in firegoat.yaml adds
8979
+ claude flags, e.g. --chrome or --plugin-dir). claude saves its session there. There
8980
+ is no sandbox: the agent can still reach the rest of your machine, including the
8981
+ repo, if it goes looking.
8982
+
8983
+ codex-cli runs codex exec --json --dangerously-bypass-approvals-and-sandbox with a
8984
+ fresh CODEX_HOME, ~/.firegoat/runs/<hash>/.codex, holding a link to your codex login
8985
+ (~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
8986
+ -c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
8987
+ yours. codex's own HOME is the run's home (below), also for its Read/Write. It
8988
+ saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai doesn't
8989
+ read codex sessions yet: those runs are not uploaded. codex reports no turns or
8990
+ cost; its model comes from the session file.
8991
+
8992
+ grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
8993
+ PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
8994
+ --output-format streaming-messages-json --always-approve --no-auto-update, with a
8995
+ fresh GROK_HOME, ~/.firegoat/runs/<hash>/.grok, holding a link to your grok login
8996
+ ($GROK_HOME/auth.json or ~/.grok/auth.json; run grok login first; XAI_API_KEY alone
8997
+ does not log headless grok in). grok's own HOME is the run's home (below): with
8998
+ your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eval
8999
+ refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
9000
+ a refresh inside a run would replace the link with a file, which the next eval
9001
+ asks you to move back over ~/.grok/auth.json. The session is a directory,
9002
+ $GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). firegoat.ai doesn't read
9003
+ grok sessions yet: those runs are not uploaded.
9004
+
9005
+ Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
9006
+ (with XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
8258
9007
  $FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
8259
9008
  collide. claude itself keeps your real HOME (its login is there) and sources
8260
- <n>/home.env (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
9009
+ home.env next to it (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
8261
9010
  checks and cleanup get the run's HOME directly; build keeps yours. Limits: claude's
8262
9011
  own Read/Write tools expand ~ to your real home; getpwuid lookups (os.userInfo,
8263
9012
  python pwd) still see it; keychain logins (gh, docker, git credential helpers) look
@@ -8273,14 +9022,18 @@ nested session. Login and provider vars stay: ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOK
8273
9022
  CLAUDE_CODE_OAUTH_REFRESH_TOKEN, CLAUDE_CODE_USE_*, CLAUDE_CODE_SKIP_*_AUTH,
8274
9023
  CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, CLAUDE_CODE_CLIENT_KEY_PASSPHRASE.
8275
9024
 
8276
- In a linked git worktree (git worktree add), .firegoat/runs/ and .firegoat/.claude
8277
- live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
8278
- fixtures and build: stay in the current folder.
9025
+ In a linked git worktree (git worktree add), .firegoat/runs/ lives in the main
9026
+ checkout instead, and <hash> is the main checkout's, so runs outlive the worktree;
9027
+ firegoat.yaml, fixtures and build: stay in the current folder.
8279
9028
 
8280
9029
  test.. Only these tests (by name)
8281
9030
  --runs N Runs per test (overrides runs: in firegoat.yaml)
8282
- --model NAME claude --model, replacing model: in firegoat.yaml; repeat it
8283
- (--model a --model b) to run each test runs times per model
9031
+ --harness NAME claude-code, codex-cli or grok-cli, replacing harness: in
9032
+ firegoat.yaml;
9033
+ repeat it to run each test on each harness
9034
+ --model NAME The harness's --model, replacing model: in firegoat.yaml;
9035
+ repeat it (--model a --model b) to run each test runs times
9036
+ per model
8284
9037
  --parallel N Runs at once (default 4; 1 runs them one after another).
8285
9038
  Each run has its own HOME; state outside it (a fixed port,
8286
9039
  /tmp paths) can still collide: use 1 then
@@ -8292,19 +9045,25 @@ fixtures and build: stay in the current folder.
8292
9045
  ("pass", "fail" or "error" = not graded) and exitCode
8293
9046
  --no-upload Keep runs local even when logged in to firegoat.ai
8294
9047
  --dry-run Check firegoat.yaml as eval would (test names, --var,
8295
- --model, missing vars), print each planned run
8296
- ("<test>#<n> [model]" with its install, checks and cleanup
8297
- counts and rendered claude_args, paths under
8298
- runs/<runId>/) and exit 0. Builds nothing, starts no
8299
- claude, uploads nothing, writes no files. Config errors
9048
+ --model, --harness, missing vars), print each planned
9049
+ run ("<test>#<n> [harness model]" with its install, checks
9050
+ and cleanup counts and rendered harness_args, paths under
9051
+ ~/.firegoat/runs/<hash>/<random>/) and exit 0. Builds
9052
+ nothing, starts no agent, uploads nothing, writes no
9053
+ files. Config errors
8300
9054
  exit 3. With --json: {dryRun, build, runs: [{test, n,
8301
- model, claudeArgs, install, checks, cleanup}]}, steps
8302
- and checks with their placeholders filled
8303
- --claude-bin PATH Path to claude (default: claude on PATH)
8304
-
8305
- Upload: after firegoat login (or with FIREGOAT_API_KEY), each run's claude session,
8306
- checks.txt and stderr.txt go to firegoat.ai as a private trace, and its URL is
8307
- printed ("✓ <test>#<n> <url>", or "✗ ... Upload failed: <why>") and saved as
9055
+ harness, model, harnessArgs, install, checks,
9056
+ cleanup}]}, steps and checks with their placeholders
9057
+ filled
9058
+ --harness-bin PATH Path to the harness binary (default: claude or codex on
9059
+ PATH, ~/.grok/bin/grok); only with exactly one harness
9060
+
9061
+ Run labels: "PASS <test>#<n> [<harness> <model>]": the harness shows when the eval
9062
+ runs more than one, or not claude-code; the model when one is set.
9063
+
9064
+ Upload: after firegoat login (or with FIREGOAT_API_KEY), each claude-code run's
9065
+ session, checks.txt and stderr.txt go to firegoat.ai as a private trace, and its
9066
+ URL is printed ("✓ <test>#<n> <url>", or "✗ ... Upload failed: <why>") and saved as
8308
9067
  upload in run.json. Before any run, eval checks the key: a 401/403 exits 3, any
8309
9068
  other failure (unreachable, 429, 5xx, wrong FIREGOAT_BASE_URL) exits 2 (fix it, or
8310
9069
  --no-upload). A failed upload never changes a run's status or the exit code; eval
@@ -8313,27 +9072,32 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
8313
9072
  (default https://firegoat.ai/evals). See firegoat login --help.
8314
9073
 
8315
9074
  Output: .firegoat/runs/<runId>/
9075
+ out A symlink to $FIREGOAT_OUT,
9076
+ ~/.firegoat/runs/<hash>/<random>/out
8316
9077
  summary.json firegoatVersion, runId, runsDir, dir (where eval
8317
9078
  ran), git {branch, commit, dirty} (no git outside a
8318
- repo), upload {origin, email} (null when not
8319
- uploading), runs (every run.json), then the verdict:
8320
- totals, status and exitCode
9079
+ repo), harnesses, upload {origin, email} (null when
9080
+ not uploading), runs (every run.json), then the
9081
+ verdict: totals, status and exitCode
8321
9082
  ../index.jsonl One line per eval: runId, startedAt, dir, branch,
8322
- commit, passed, failed, notGraded, cost (null when
9083
+ commit, harnesses, passed, failed, notGraded, cost (null when
8323
9084
  any run's cost is unknown), uploaded, uploadFailed
8324
- <test>/<n>/run.json One run: status and reason (from the checks), model
8325
- (asked for, or null) and claudeModel (what claude's
8326
- init line reported), claudeArgs (claude_args
8327
- passed), prompt, end_goal,
9085
+ <test>/<n>/run.json One run: status and reason (from the checks), harness,
9086
+ model (asked for, or null) and reportedModel (what
9087
+ the harness reported running, or null), harnessArgs
9088
+ (harness_args passed), prompt, end_goal,
8328
9089
  checks [{run, code}], cleanup [{run, code}],
8329
- and paths: workdir, home (the run's HOME),
8330
- transcript, session (claude's session file), stderr,
9090
+ and paths: dir (this folder), workdir, home (the
9091
+ run's HOME; both real paths, under ~/.firegoat),
9092
+ transcript, session (the harness's session file), stderr,
8331
9093
  checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
8332
9094
  url} | {status: "failed", error} | {status:
8333
9095
  "skipped", reason}, null when not uploading)
8334
9096
  <test>/<n>/transcript.txt What the agent did, below
8335
- <test>/<n>/home/, home.env The run's HOME, and the file claude sources
8336
- before the agent's Bash commands
9097
+ <test>/<n>/work, home Symlinks to the agent's folder and the run's HOME
9098
+ (home.env, which claude sources before the agent's
9099
+ Bash commands, is next to them)
9100
+
8337
9101
  build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
8338
9102
 
8339
9103
  Transcript ($FIREGOAT_TRANSCRIPT), greppable line by line; n counts tool calls:
@@ -8359,6 +9123,8 @@ Examples:
8359
9123
  npx firegoat eval quickstart --runs 3 One test, three runs
8360
9124
  npx firegoat eval --model claude-haiku-5-5 --model claude-sonnet-5-5
8361
9125
  Each test on both models
9126
+ npx firegoat eval --harness claude-code --harness codex-cli
9127
+ Each test on claude and on codex
8362
9128
  npx firegoat eval --var SHA=$(git rev-parse HEAD)
8363
9129
  npx firegoat eval --dry-run Validate firegoat.yaml, list the runs
8364
9130
 
@@ -8387,30 +9153,6 @@ Example:
8387
9153
 
8388
9154
  Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
8389
9155
 
8390
- // src/home.ts
8391
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
8392
- import { join as join4 } from "node:path";
8393
- function homeEnv(home) {
8394
- return {
8395
- HOME: home,
8396
- XDG_CONFIG_HOME: join4(home, ".config"),
8397
- XDG_CACHE_HOME: join4(home, ".cache"),
8398
- XDG_DATA_HOME: join4(home, ".local", "share"),
8399
- XDG_STATE_HOME: join4(home, ".local", "state")
8400
- };
8401
- }
8402
- var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
8403
- function makeRunHome(dir) {
8404
- const home = join4(dir, "home");
8405
- const env = homeEnv(home);
8406
- for (const d of Object.values(env))
8407
- mkdirSync3(d, { recursive: true });
8408
- const envFile = join4(dir, "home.env");
8409
- writeFileSync2(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
8410
- `).join(""));
8411
- return { home, envFile };
8412
- }
8413
-
8414
9156
  // src/steps.ts
8415
9157
  import { createWriteStream } from "node:fs";
8416
9158
  var STEP_TIMEOUT_MS = 600000;
@@ -8545,8 +9287,8 @@ async function sendTelemetry(event, exitCode, startedAt) {
8545
9287
  }
8546
9288
 
8547
9289
  // src/transcript.ts
8548
- import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
8549
- import { join as join5, relative } from "node:path";
9290
+ import { readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
9291
+ import { join as join8, relative } from "node:path";
8550
9292
  var OUTPUT_LINES = 15;
8551
9293
  var OUTPUT_CHARS = 1500;
8552
9294
  var TEXT_CHARS = 1500;
@@ -8572,7 +9314,15 @@ var prose = (text) => prefixLines("agent: ", text);
8572
9314
  function describeCall(step, name, input) {
8573
9315
  if (typeof input.command === "string")
8574
9316
  return prefixLines(`[${step}] $ `, input.command);
8575
- for (const key of ["file_path", "path", "url", "pattern", "query"])
9317
+ for (const key of [
9318
+ "file_path",
9319
+ "path",
9320
+ "target_file",
9321
+ "target_directory",
9322
+ "url",
9323
+ "pattern",
9324
+ "query"
9325
+ ])
8576
9326
  if (typeof input[key] === "string")
8577
9327
  return `[${step}] ${name} ${input[key]}`;
8578
9328
  return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, INPUT_CHARS)}`;
@@ -8587,11 +9337,11 @@ function describeToolCall(step, c) {
8587
9337
  function workdirFiles(dir) {
8588
9338
  const out = [];
8589
9339
  const walk = (d) => {
8590
- for (const name of readdirSync2(d)) {
9340
+ for (const name of readdirSync4(d)) {
8591
9341
  if (SKIP_DIRS.has(name))
8592
9342
  continue;
8593
- const p = join5(d, name);
8594
- if (statSync(p).isDirectory())
9343
+ const p = join8(d, name);
9344
+ if (statSync2(p).isDirectory())
8595
9345
  walk(p);
8596
9346
  else
8597
9347
  out.push(relative(dir, p));
@@ -8665,6 +9415,10 @@ function tier(rows) {
8665
9415
  const share = graded.filter((r) => r.state === "pass").length / graded.length;
8666
9416
  return share === 1 ? "S" : share >= 0.75 ? "A" : share >= 0.5 ? "B" : "C";
8667
9417
  }
9418
+ function runTag(harness, model) {
9419
+ const parts = [harness, model].filter((x) => x !== null);
9420
+ return parts.length > 0 ? ` [${parts.join(" ")}]` : "";
9421
+ }
8668
9422
  function notGradedWhy(reason) {
8669
9423
  const said = /claude result is an error \(subtype=[^)]*\): (.*)$/s.exec(reason);
8670
9424
  if (said)
@@ -8750,8 +9504,7 @@ function plainUI(write) {
8750
9504
  printed.add(r);
8751
9505
  const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
8752
9506
  const u = usageParts(r).join(", ");
8753
- const model = r.model === null ? "" : ` [${r.model}]`;
8754
- write(`${label} ${r.test}#${r.n}${model} ${r.reason}${u ? ` (${u})` : ""}`);
9507
+ write(`${label} ${r.test}#${r.n}${runTag(r.harness, r.model)} ${r.reason}${u ? ` (${u})` : ""}`);
8755
9508
  }
8756
9509
  for (const r of rows) {
8757
9510
  if (uploadPrinted.has(r) || !r.upload || r.upload === "uploading")
@@ -8826,11 +9579,14 @@ function ttyUI(out, opts) {
8826
9579
  const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
8827
9580
  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` : ""}`);
8828
9581
  }
8829
- const modelCols = Math.min(24, Math.max(0, ...rows.map((r) => r.model?.length ?? 0)));
9582
+ const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
9583
+ const harnessCols = column((r) => r.harness?.length ?? 0);
9584
+ const modelCols = column((r) => r.model?.length ?? 0);
8830
9585
  rows.forEach((r, i) => {
8831
9586
  out.push(p(C.gray, " │"));
8832
9587
  const num = String(i + 1).padStart(2, "0");
8833
9588
  const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
9589
+ const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
8834
9590
  const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
8835
9591
  let detail = "";
8836
9592
  if (r.state === "running")
@@ -8839,7 +9595,7 @@ function ttyUI(out, opts) {
8839
9595
  detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
8840
9596
  else if (r.state === "error")
8841
9597
  detail = p(C.gray, usage(r));
8842
- out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
9598
+ out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
8843
9599
  if (r.state === "running" && r.command)
8844
9600
  out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
8845
9601
  if (r.state === "fail")
@@ -8919,7 +9675,7 @@ function ttyUI(out, opts) {
8919
9675
  }
8920
9676
 
8921
9677
  // src/upload.ts
8922
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
9678
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
8923
9679
  import { basename as basename2 } from "node:path";
8924
9680
  var MAX_FILE = 20 * 1024 * 1024;
8925
9681
  var MAX_CHECKS = 200;
@@ -8947,7 +9703,7 @@ function projectName(v) {
8947
9703
  function runModel(r) {
8948
9704
  return {
8949
9705
  requested: label(r.model, "Model"),
8950
- reported: label(r.claudeModel, "Claude's reported model")
9706
+ reported: label(r.reportedModel, "Reported model")
8951
9707
  };
8952
9708
  }
8953
9709
  function runGit(g) {
@@ -9001,7 +9757,7 @@ async function uploadRun(t, r) {
9001
9757
  };
9002
9758
  const bytes = {};
9003
9759
  for (const name of FILES) {
9004
- const b = existsSync3(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
9760
+ const b = existsSync5(paths[name]) ? readFileSync5(paths[name]) : Buffer.alloc(0);
9005
9761
  if (b.byteLength > MAX_FILE)
9006
9762
  return {
9007
9763
  status: "failed",
@@ -9083,13 +9839,13 @@ async function uploadRun(t, r) {
9083
9839
  }
9084
9840
 
9085
9841
  // src/which.ts
9086
- import { accessSync, constants, statSync as statSync2 } from "node:fs";
9087
- import { delimiter, join as join6, resolve } from "node:path";
9842
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
9843
+ import { delimiter, join as join9, resolve } from "node:path";
9088
9844
  function resolveBin(bin) {
9089
9845
  const executable = (p) => {
9090
9846
  try {
9091
9847
  accessSync(p, constants.X_OK);
9092
- return statSync2(p).isFile();
9848
+ return statSync3(p).isFile();
9093
9849
  } catch {
9094
9850
  return false;
9095
9851
  }
@@ -9097,8 +9853,8 @@ function resolveBin(bin) {
9097
9853
  if (bin.includes("/"))
9098
9854
  return executable(bin) ? resolve(bin) : undefined;
9099
9855
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
9100
- if (dir && executable(join6(dir, bin)))
9101
- return join6(dir, bin);
9856
+ if (dir && executable(join9(dir, bin)))
9857
+ return join9(dir, bin);
9102
9858
  }
9103
9859
  return;
9104
9860
  }
@@ -9112,12 +9868,12 @@ function parseArgs(args) {
9112
9868
  const opts = {
9113
9869
  names: [],
9114
9870
  models: [],
9871
+ harnesses: [],
9115
9872
  parallel: 4,
9116
9873
  json: false,
9117
9874
  noUpload: false,
9118
9875
  dryRun: false,
9119
- vars: {},
9120
- claudeBin: "claude"
9876
+ vars: {}
9121
9877
  };
9122
9878
  for (let i = 0;i < args.length; i++) {
9123
9879
  const a = args[i];
@@ -9158,14 +9914,21 @@ function parseArgs(args) {
9158
9914
  if (opts.models.includes(v))
9159
9915
  throw new UsageError(`--model ${v} given twice`);
9160
9916
  opts.models.push(v);
9917
+ } else if (flag === "--harness") {
9918
+ const v = value();
9919
+ if (!HARNESSES.has(v))
9920
+ throw new UsageError(`Unknown harness ${JSON.stringify(v)} ${validHarnesses()}`);
9921
+ if (opts.harnesses.includes(v))
9922
+ throw new UsageError(`--harness ${v} given twice`);
9923
+ opts.harnesses.push(v);
9161
9924
  } else if (flag === "--parallel") {
9162
9925
  const v = value();
9163
9926
  const n = Number(v);
9164
9927
  if (!/^\d+$/.test(v) || n < 1)
9165
9928
  throw new UsageError(`--parallel must be a positive integer, got ${v}`);
9166
9929
  opts.parallel = n;
9167
- } else if (flag === "--claude-bin")
9168
- opts.claudeBin = value();
9930
+ } else if (flag === "--harness-bin")
9931
+ opts.harnessBin = value();
9169
9932
  else if (a.startsWith("-"))
9170
9933
  throw new UsageError(`Unknown flag: ${a}`);
9171
9934
  else
@@ -9178,7 +9941,7 @@ function newRunId() {
9178
9941
  return `${ts}-${randomBytes2(2).toString("hex")}`;
9179
9942
  }
9180
9943
  function writeRunJson(r) {
9181
- writeFileSync4(join7(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
9944
+ writeFileSync4(join10(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
9182
9945
  `);
9183
9946
  }
9184
9947
  async function uploadPreflight(opts, ui, project, runGroup, git) {
@@ -9219,18 +9982,18 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
9219
9982
  };
9220
9983
  }
9221
9984
  }
9222
- function runFill(runId, n, testName, out, dir) {
9985
+ function runFill(runId, n, testName, out, agentDir) {
9223
9986
  return {
9224
9987
  "run.id": runId,
9225
9988
  "run.n": String(n),
9226
9989
  "test.name": testName,
9227
9990
  FIREGOAT_OUT: out,
9228
- FIREGOAT_WORKDIR: join7(dir, "work"),
9229
- FIREGOAT_HOME: join7(dir, "home")
9991
+ FIREGOAT_WORKDIR: join10(agentDir, "work"),
9992
+ FIREGOAT_HOME: join10(agentDir, "home")
9230
9993
  };
9231
9994
  }
9232
- function renderClaudeArgs(test, vars, fill) {
9233
- return test.claudeArgs.map((a, i) => renderValue(a, vars, fill, `claude_args[${i}]`));
9995
+ function renderHarnessArgs(test, vars, fill) {
9996
+ return test.harnessArgs.map((a) => renderValue(a.arg, vars, fill, a.path));
9234
9997
  }
9235
9998
  function renderSteps(what, commands, vars, fill) {
9236
9999
  return commands.map((c, i) => renderStep(c, vars, fill, `${what}[${i}]`));
@@ -9245,24 +10008,29 @@ function renderRunSteps(test, top, vars, fill) {
9245
10008
  function renderBuild(config, vars, runId) {
9246
10009
  return renderSteps("build", config.build, vars, { "run.id": runId });
9247
10010
  }
9248
- function planRuns(tests, models, runs) {
9249
- const matrix = models.length > 0 ? models : [null];
9250
- return tests.flatMap((t) => matrix.flatMap((model, m) => Array.from({ length: runs }, (_, i) => ({
10011
+ function planRuns(tests, harnesses, models, runs) {
10012
+ const modelList = models.length > 0 ? models : [null];
10013
+ const matrix = harnesses.flatMap((harness) => modelList.map((model) => ({ harness, model })));
10014
+ return tests.flatMap((t) => matrix.flatMap((cell, c) => Array.from({ length: runs }, (_, i) => ({
9251
10015
  test: t.name,
9252
- n: m * runs + i + 1,
9253
- model
10016
+ n: c * runs + i + 1,
10017
+ ...cell
9254
10018
  }))));
9255
10019
  }
10020
+ function showsHarness(harnesses) {
10021
+ return harnesses.length > 1 || harnesses[0] !== DEFAULT_HARNESS;
10022
+ }
9256
10023
  var DRY_RUN_ID = "<runId>";
9257
10024
  function dryRun(opts) {
9258
- const out = join7(opts.runsRoot, DRY_RUN_ID, "out");
10025
+ const agentDir = join10(opts.agentDirs, "<random>");
10026
+ const out = join10(agentDir, "out");
9259
10027
  const runs = opts.plan.map((p) => {
9260
10028
  const test = opts.tests.find((t) => t.name === p.test);
9261
- const fill = runFill(DRY_RUN_ID, p.n, test.name, out, join7(opts.runsRoot, DRY_RUN_ID, test.name, String(p.n)));
10029
+ const fill = runFill(DRY_RUN_ID, p.n, test.name, out, agentDir);
9262
10030
  renderEnv(test.env, opts.vars, fill);
9263
10031
  return {
9264
10032
  ...p,
9265
- claudeArgs: renderClaudeArgs(test, opts.vars, fill),
10033
+ harnessArgs: renderHarnessArgs(test, opts.vars, fill),
9266
10034
  ...renderRunSteps(test, opts.config, opts.vars, fill)
9267
10035
  };
9268
10036
  });
@@ -9274,22 +10042,27 @@ function dryRun(opts) {
9274
10042
  const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
9275
10043
  opts.say(`Dry run: ${count(runs.length, "run")}, build ${count(opts.config.build.length, "step")}. Nothing was started.`);
9276
10044
  for (const r of runs)
9277
- opts.say(`${r.test}#${r.n}${r.model === null ? "" : ` [${r.model}]`} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.claudeArgs.length > 0 ? `, claude_args ${JSON.stringify(r.claudeArgs)}` : ""}`);
10045
+ opts.say(`${r.test}#${r.n}${runTag(opts.showHarness ? r.harness : null, r.model)} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.harnessArgs.length > 0 ? `, harness_args ${JSON.stringify(r.harnessArgs)}` : ""}`);
9278
10046
  }
9279
10047
  async function runOne(opts) {
9280
10048
  const { test, n } = opts;
9281
10049
  const started = Date.now();
9282
- const dir = join7(opts.runDir, test.name, String(n));
9283
- const workdir = join7(dir, "work");
9284
- mkdirSync4(workdir, { recursive: true });
9285
- const { home, envFile } = makeRunHome(dir);
9286
- const fill = runFill(opts.runId, n, test.name, opts.out, dir);
10050
+ const dir = join10(opts.runDir, test.name, String(n));
10051
+ mkdirSync5(dir, { recursive: true });
10052
+ const agentDir = makeAgentDir(opts.agentDirs);
10053
+ const workdir = join10(agentDir, "work");
10054
+ mkdirSync5(workdir);
10055
+ const { home, envFile } = makeRunHome(agentDir);
10056
+ linkDir(workdir, join10(dir, "work"));
10057
+ linkDir(home, join10(dir, "home"));
10058
+ const fill = runFill(opts.runId, n, test.name, opts.out, agentDir);
9287
10059
  const r = {
9288
10060
  test: test.name,
9289
10061
  n,
10062
+ harness: opts.harness.name,
9290
10063
  model: opts.model,
9291
- claudeModel: null,
9292
- claudeArgs: renderClaudeArgs(test, opts.vars, fill),
10064
+ reportedModel: null,
10065
+ harnessArgs: renderHarnessArgs(test, opts.vars, fill),
9293
10066
  status: "error",
9294
10067
  reason: "",
9295
10068
  prompt: render(test.prompt, opts.vars),
@@ -9299,11 +10072,11 @@ async function runOne(opts) {
9299
10072
  workdir,
9300
10073
  home,
9301
10074
  session: null,
9302
- transcript: join7(dir, "transcript.txt"),
9303
- stderr: join7(dir, "stderr.txt"),
9304
- checksLog: join7(dir, "checks.txt"),
10075
+ transcript: join10(dir, "transcript.txt"),
10076
+ stderr: join10(dir, "stderr.txt"),
10077
+ checksLog: join10(dir, "checks.txt"),
9305
10078
  cleanup: [],
9306
- cleanupLog: join7(dir, "cleanup.txt"),
10079
+ cleanupLog: join10(dir, "cleanup.txt"),
9307
10080
  num_turns: null,
9308
10081
  cost: null,
9309
10082
  duration_ms: 0,
@@ -9316,7 +10089,9 @@ async function runOne(opts) {
9316
10089
  ...env,
9317
10090
  ...homeEnv(home),
9318
10091
  FIREGOAT_WORKDIR: workdir,
9319
- FIREGOAT_HOME: home
10092
+ FIREGOAT_HOME: home,
10093
+ FIREGOAT_RUN_DIR: dir,
10094
+ FIREGOAT_REPO: opts.repo
9320
10095
  };
9321
10096
  const steps = renderRunSteps(test, opts, opts.vars, fill);
9322
10097
  const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile);
@@ -9333,6 +10108,9 @@ async function runOne(opts) {
9333
10108
  writeRunJson(r);
9334
10109
  return r;
9335
10110
  }
10111
+ function withoutOldPwd(env) {
10112
+ return Object.fromEntries(Object.entries(env).filter(([k]) => k !== "OLDPWD"));
10113
+ }
9336
10114
  async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
9337
10115
  const { test, n } = opts;
9338
10116
  const { dir, workdir } = r;
@@ -9342,7 +10120,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
9342
10120
  commands: steps.install,
9343
10121
  cwd: workdir,
9344
10122
  env: stepEnv,
9345
- logPath: join7(dir, "install.txt")
10123
+ logPath: join10(dir, "install.txt")
9346
10124
  });
9347
10125
  if (error)
9348
10126
  return ["error", `not graded: ${error}`];
@@ -9354,9 +10132,13 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
9354
10132
  prompt: r.prompt,
9355
10133
  cwd: workdir,
9356
10134
  env: {
9357
- ...hostEnv(),
10135
+ ...withoutOldPwd(hostEnv()),
9358
10136
  ...env,
9359
- ...harness.env({ configDir: opts.configDir, homeEnvFile }),
10137
+ ...harness.env({
10138
+ configDir: opts.configDir,
10139
+ run: { home: r.home, homeEnvFile }
10140
+ }),
10141
+ PWD: workdir,
9360
10142
  PATH: opts.stepEnv.PATH,
9361
10143
  FIREGOAT_OUT: opts.out,
9362
10144
  FIREGOAT_WORKDIR: workdir,
@@ -9365,18 +10147,22 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
9365
10147
  stderr,
9366
10148
  timeoutMs: test.timeout * 1000,
9367
10149
  model: opts.model ?? undefined,
9368
- extraArgs: r.claudeArgs,
10150
+ extraArgs: r.harnessArgs,
9369
10151
  onProgress: opts.onProgress
9370
10152
  });
9371
- r.claudeModel = outcome.reportedModel;
9372
10153
  if (outcome.version !== null)
9373
10154
  evalStats({ claudeVersion: claudeVersion(outcome.version) });
9374
10155
  await new Promise((res) => stderr.end(res));
9375
10156
  if (outcome.sessionId)
9376
10157
  r.session = harness.findSession(opts.configDir, outcome.sessionId);
10158
+ r.reportedModel = outcome.reportedModel ?? (r.session && harness.sessionModel ? harness.sessionModel(r.session) : null);
10159
+ const about = [
10160
+ opts.shownHarness,
10161
+ opts.model === null ? null : `model ${opts.model}`
10162
+ ].filter((x) => x !== null);
9377
10163
  writeTranscript({
9378
10164
  path: r.transcript,
9379
- title: `${test.name}#${n}${opts.model ? ` (model ${opts.model})` : ""}`,
10165
+ title: `${test.name}#${n}${about.length > 0 ? ` (${about.join(", ")})` : ""}`,
9380
10166
  prompt: r.prompt,
9381
10167
  steps: outcome.steps,
9382
10168
  final: outcome.final,
@@ -9419,6 +10205,9 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
9419
10205
  test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
9420
10206
  ];
9421
10207
  }
10208
+ function notUploaded(h) {
10209
+ return `firegoat.ai doesn't read ${h.name} sessions yet: its runs stay local`;
10210
+ }
9422
10211
  var MAX_LISTED_URLS = 5;
9423
10212
  function uploadedSummary(origin, results, runDir) {
9424
10213
  const urls = results.flatMap((r) => r.upload?.status === "uploaded" ? [r.upload.url] : []);
@@ -9426,7 +10215,7 @@ function uploadedSummary(origin, results, runDir) {
9426
10215
  return;
9427
10216
  const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
9428
10217
  if (urls.length > MAX_LISTED_URLS) {
9429
- console.log(`${head}: each run's upload.url in ${join7(runDir, "summary.json")}`);
10218
+ console.log(`${head}: each run's upload.url in ${join10(runDir, "summary.json")}`);
9430
10219
  return;
9431
10220
  }
9432
10221
  console.log(`${head}:`);
@@ -9481,9 +10270,22 @@ async function evalCommand(args, root) {
9481
10270
  }
9482
10271
  const runs = opts.runs ?? config.runs;
9483
10272
  const models = opts.models.length > 0 ? opts.models : config.models;
10273
+ const harnessNames = opts.harnesses.length > 0 ? opts.harnesses : config.harnesses;
10274
+ if (opts.harnessBin !== undefined && harnessNames.length !== 1)
10275
+ return fatal(EXIT.usage, `--harness-bin needs exactly one harness, this eval runs ${harnessNames.length} (${harnessNames.join(", ")}): pick one with --harness`, "usage");
10276
+ const harnesses = harnessNames.map((h) => HARNESSES.get(h));
10277
+ const showHarness = showsHarness(harnessNames);
10278
+ try {
10279
+ for (const h of harnesses)
10280
+ checkHarnessArgs(tests, h);
10281
+ } catch (e) {
10282
+ if (e instanceof ConfigError)
10283
+ return fatal(EXIT.usage, e.message, "config");
10284
+ throw e;
10285
+ }
9484
10286
  evalStats({
9485
10287
  testCount: tests.length,
9486
- runCount: tests.length * runs * Math.max(models.length, 1),
10288
+ runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
9487
10289
  modelCount: models.length,
9488
10290
  parallel: opts.parallel
9489
10291
  });
@@ -9493,7 +10295,7 @@ async function evalCommand(args, root) {
9493
10295
  t.prompt,
9494
10296
  ...t.checks.map((c) => c.run),
9495
10297
  ...Object.values(t.env),
9496
- ...t.claudeArgs,
10298
+ ...t.harnessArgs.map((a) => a.arg),
9497
10299
  ...t.install,
9498
10300
  ...t.cleanup
9499
10301
  ]).concat(config.build, config.install, config.cleanup).flatMap(varRefs).filter((k) => !(k in vars)))
@@ -9511,9 +10313,10 @@ async function evalCommand(args, root) {
9511
10313
  dryRun({
9512
10314
  config,
9513
10315
  tests,
9514
- plan: planRuns(tests, models, runs),
10316
+ plan: planRuns(tests, harnessNames, models, runs),
10317
+ showHarness,
9515
10318
  vars,
9516
- runsRoot: join7(repo?.mainCheckout ?? root, ".firegoat", "runs"),
10319
+ agentDirs: projectAgentDir(repo?.mainCheckout ?? root),
9517
10320
  json: opts.json,
9518
10321
  say
9519
10322
  });
@@ -9524,28 +10327,42 @@ async function evalCommand(args, root) {
9524
10327
  }
9525
10328
  return EXIT.pass;
9526
10329
  }
9527
- const harness = claude;
9528
- evalStats({ harness: harness.name });
9529
- const binPath = resolveBin(opts.claudeBin);
9530
- if (!binPath)
9531
- return fatal(EXIT.notGraded, `Not graded: ${harness.name} binary ${JSON.stringify(opts.claudeBin)} not found. ${harness.installHint}`, "claude_missing");
10330
+ evalStats({ harness: harnessNames.join(",") });
10331
+ const bins = new Map;
10332
+ for (const h of harnesses) {
10333
+ const bin = opts.harnessBin ?? h.bin;
10334
+ const path = resolveBin(bin);
10335
+ if (!path)
10336
+ return fatal(EXIT.notGraded, `Not graded: ${h.name} binary ${JSON.stringify(bin)} not found. ${h.installHint}`, "harness_missing");
10337
+ bins.set(h, path);
10338
+ }
9532
10339
  let repo;
9533
10340
  try {
9534
10341
  repo = gitRepo(root);
9535
10342
  } catch (e) {
9536
10343
  return fatal(EXIT.usage, e.message, "config");
9537
10344
  }
9538
- const firegoatDir = ensureFiregoatDir(repo?.mainCheckout ?? root);
9539
- const configDir = join7(firegoatDir, harness.configDirName);
9540
- mkdirSync4(configDir, { recursive: true });
9541
- ui.auth(harness.authLabel());
9542
- const loginProblem = harness.checkLogin({
9543
- bin: binPath,
9544
- env: { ...hostEnv(), ...harness.env({ configDir }) },
9545
- cwd: root
9546
- });
9547
- if (loginProblem)
9548
- return fatal(EXIT.notGraded, `Not graded: ${loginProblem.reason}`, loginProblem.loggedOut ? "claude_logged_out" : "claude_auth_status");
10345
+ const project = repo?.mainCheckout ?? root;
10346
+ const firegoatDir = ensureFiregoatDir(project);
10347
+ const agentDirs = ensureProjectAgentDir(project);
10348
+ const configDirs = new Map;
10349
+ for (const h of harnesses) {
10350
+ const configDir = join10(agentDirs, h.configDirName);
10351
+ mkdirSync5(configDir, { recursive: true });
10352
+ configDirs.set(h, configDir);
10353
+ }
10354
+ ui.auth(harnesses.length === 1 ? harnesses[0].authLabel() : harnesses.map((h) => `${h.name}: ${h.authLabel()}`).join("; "));
10355
+ for (const h of harnesses) {
10356
+ const configDir = configDirs.get(h);
10357
+ const loginProblem = h.checkLogin({
10358
+ bin: bins.get(h),
10359
+ env: { ...hostEnv(), ...h.env({ configDir }) },
10360
+ cwd: root,
10361
+ configDir
10362
+ });
10363
+ if (loginProblem)
10364
+ return fatal(EXIT.notGraded, `Not graded: ${loginProblem.reason}`, loginProblem.loggedOut ? "harness_logged_out" : "harness_auth_status");
10365
+ }
9549
10366
  const evalStarted = Date.now();
9550
10367
  const runId = newRunId();
9551
10368
  let upload;
@@ -9554,23 +10371,32 @@ async function evalCommand(args, root) {
9554
10371
  if (pre && "exitCode" in pre)
9555
10372
  return fatal(pre.exitCode, pre.error, pre.code);
9556
10373
  upload = pre;
10374
+ if (upload) {
10375
+ for (const h of harnesses)
10376
+ if (!h.uploads)
10377
+ ui.note(notUploaded(h));
10378
+ }
9557
10379
  } catch (e) {
9558
10380
  if (e instanceof ConfigError)
9559
10381
  return fatal(EXIT.usage, e.message, "config");
9560
10382
  throw e;
9561
10383
  }
9562
- const runsRoot = join7(firegoatDir, "runs");
9563
- const runDir = join7(runsRoot, runId);
9564
- const out = join7(runDir, "out");
9565
- mkdirSync4(out, { recursive: true });
10384
+ const runsRoot = join10(firegoatDir, "runs");
10385
+ const runDir = join10(runsRoot, runId);
10386
+ mkdirSync5(runDir, { recursive: true });
10387
+ const out = join10(makeAgentDir(agentDirs), "out");
10388
+ mkdirSync5(out);
10389
+ linkDir(out, join10(runDir, "out"));
9566
10390
  const stepEnv = {
9567
10391
  ...hostEnv(),
9568
10392
  ...vars,
9569
10393
  FIREGOAT_OUT: out,
9570
10394
  PATH: [out, process.env.PATH ?? ""].join(delimiter2)
9571
10395
  };
9572
- const rows = planRuns(tests, models, runs).map((p) => ({
10396
+ const plan = planRuns(tests, harnessNames, models, runs);
10397
+ const rows = plan.map((p) => ({
9573
10398
  ...p,
10399
+ harness: showHarness ? p.harness : null,
9574
10400
  state: "queued",
9575
10401
  turns: 0
9576
10402
  }));
@@ -9585,7 +10411,7 @@ async function evalCommand(args, root) {
9585
10411
  ...stepEnv,
9586
10412
  ...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
9587
10413
  },
9588
- logPath: join7(runDir, "build.txt")
10414
+ logPath: join10(runDir, "build.txt")
9589
10415
  });
9590
10416
  const secs = Math.round((Date.now() - buildStarted) / 1000);
9591
10417
  ui.build(error ? "fail" : "ok", secs);
@@ -9597,6 +10423,7 @@ async function evalCommand(args, root) {
9597
10423
  const runAt = async (i) => {
9598
10424
  const row = rows[i];
9599
10425
  const test = tests.find((t) => t.name === row.test);
10426
+ const harness = HARNESSES.get(plan[i].harness);
9600
10427
  row.state = "running";
9601
10428
  ui.update();
9602
10429
  const r = await runOne({
@@ -9608,11 +10435,14 @@ async function evalCommand(args, root) {
9608
10435
  test,
9609
10436
  n: row.n,
9610
10437
  model: row.model,
10438
+ shownHarness: row.harness,
9611
10439
  runId,
9612
10440
  runDir,
10441
+ agentDirs,
10442
+ repo: root,
9613
10443
  harness,
9614
- bin: binPath,
9615
- configDir,
10444
+ bin: bins.get(harness),
10445
+ configDir: configDirs.get(harness),
9616
10446
  install: config.install,
9617
10447
  cleanup: config.cleanup,
9618
10448
  out,
@@ -9630,7 +10460,10 @@ async function evalCommand(args, root) {
9630
10460
  command: undefined
9631
10461
  });
9632
10462
  ui.update();
9633
- if (upload) {
10463
+ if (upload && !harness.uploads) {
10464
+ r.upload = { status: "skipped", reason: notUploaded(harness) };
10465
+ writeRunJson(r);
10466
+ } else if (upload) {
9634
10467
  row.upload = "uploading";
9635
10468
  ui.update();
9636
10469
  r.upload = await uploadRun(upload, r);
@@ -9669,20 +10502,22 @@ async function evalCommand(args, root) {
9669
10502
  runsDir: runDir,
9670
10503
  dir: root,
9671
10504
  ...repo && { git: repo.info },
10505
+ harnesses: harnessNames,
9672
10506
  upload: upload && { origin: upload.origin, email: upload.email },
9673
10507
  runs: results,
9674
10508
  totals,
9675
10509
  status,
9676
10510
  exitCode
9677
10511
  };
9678
- writeFileSync4(join7(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
10512
+ writeFileSync4(join10(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
9679
10513
  `);
9680
- appendFileSync(join7(runsRoot, "index.jsonl"), `${JSON.stringify({
10514
+ appendFileSync(join10(runsRoot, "index.jsonl"), `${JSON.stringify({
9681
10515
  runId,
9682
10516
  startedAt: new Date(evalStarted).toISOString(),
9683
10517
  dir: root,
9684
10518
  branch: repo?.info.branch ?? null,
9685
10519
  commit: repo?.info.commit ?? null,
10520
+ harnesses: harnessNames,
9686
10521
  passed: totals.pass,
9687
10522
  failed: totals.fail,
9688
10523
  notGraded: totals.error,
@@ -9715,7 +10550,7 @@ async function evalCommand(args, root) {
9715
10550
  }
9716
10551
 
9717
10552
  // src/feedback.ts
9718
- import { readFileSync as readFileSync4 } from "node:fs";
10553
+ import { readFileSync as readFileSync6 } from "node:fs";
9719
10554
 
9720
10555
  // node_modules/@firegoat/feedback/dist/index.js
9721
10556
  function memoryDeviceStore() {
@@ -9934,7 +10769,7 @@ async function feedbackCommand(args) {
9934
10769
  try {
9935
10770
  if (a === "--body") {
9936
10771
  const v = value();
9937
- body = v === "-" ? readFileSync4(0, "utf8") : v;
10772
+ body = v === "-" ? readFileSync6(0, "utf8") : v;
9938
10773
  } else if (a === "--type") {
9939
10774
  const v = value();
9940
10775
  if (!TYPES.includes(v))
@@ -9987,9 +10822,9 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
9987
10822
  }
9988
10823
 
9989
10824
  // src/init.ts
9990
- import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
9991
- import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
9992
- import { basename as basename4, join as join8 } from "node:path";
10825
+ import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
10826
+ import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
10827
+ import { basename as basename4, join as join11 } from "node:path";
9993
10828
 
9994
10829
  // src/login.ts
9995
10830
  import { spawn as spawn2 } from "node:child_process";
@@ -10000,7 +10835,7 @@ var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
10000
10835
  var UPLOADS_ON = "Eval uploads each session to firegoat.ai as a private trace; --no-upload keeps runs local.";
10001
10836
  var UPLOAD_NOTICE = `${UPLOADS_ON} A session holds prompts, tool outputs and file contents: possibly secrets from your env or repo. The agent runs as you and can read ~/.firegoat (and a FIREGOAT_API_KEY in firegoat's own process env).`;
10002
10837
  var TERMS = "By logging in, you agree to the firegoat.ai Terms of Service (https://firegoat.ai/terms) and Privacy Policy (https://firegoat.ai/privacy).";
10003
- function str2(o, k, step) {
10838
+ function str3(o, k, step) {
10004
10839
  const v = o?.[k];
10005
10840
  if (typeof v !== "string" || v === "")
10006
10841
  throw new ApiError(`${step}: response has no ${k}`);
@@ -10031,9 +10866,9 @@ async function loginFlow(show, interactive) {
10031
10866
  });
10032
10867
  if (cfg.status !== 200)
10033
10868
  throw replyError(cfgStep, cfg);
10034
- const clientId = str2(cfg.json, "client_id", cfgStep);
10035
- const deviceUrl = assertSafeUrl(str2(cfg.json, "device_authorization_endpoint", cfgStep), "device_authorization_endpoint");
10036
- const tokenUrl = assertSafeUrl(str2(cfg.json, "token_endpoint", cfgStep), "token_endpoint");
10869
+ const clientId = str3(cfg.json, "client_id", cfgStep);
10870
+ const deviceUrl = assertSafeUrl(str3(cfg.json, "device_authorization_endpoint", cfgStep), "device_authorization_endpoint");
10871
+ const tokenUrl = assertSafeUrl(str3(cfg.json, "token_endpoint", cfgStep), "token_endpoint");
10037
10872
  if (tokenUrl.origin !== origin)
10038
10873
  throw new ApiError(`token_endpoint must be on ${origin}, got ${printable(tokenUrl.origin, 100)}`);
10039
10874
  const devStep = "Device authorization";
@@ -10047,11 +10882,11 @@ async function loginFlow(show, interactive) {
10047
10882
  });
10048
10883
  if (dev.status !== 200)
10049
10884
  throw replyError(devStep, dev);
10050
- const deviceCode = str2(dev.json, "device_code", devStep);
10051
- const userCode = str2(dev.json, "user_code", devStep);
10885
+ const deviceCode = str3(dev.json, "device_code", devStep);
10886
+ const userCode = str3(dev.json, "user_code", devStep);
10052
10887
  if (!/^[A-Za-z0-9-]{1,64}$/.test(userCode))
10053
10888
  throw new ApiError(`${devStep}: user_code has unexpected characters`);
10054
- const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete : str2(dev.json, "verification_uri", devStep);
10889
+ const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete : str3(dev.json, "verification_uri", devStep);
10055
10890
  const page = assertSafeUrl(pageRaw, "verification_uri");
10056
10891
  if (page.protocol !== "https:" || pageRaw.startsWith("-"))
10057
10892
  throw new ApiError("verification_uri must be https");
@@ -10288,18 +11123,23 @@ function skillsDir(scope) {
10288
11123
  if (scope === "local")
10289
11124
  return ".claude/skills/";
10290
11125
  const config = process.env.CLAUDE_CONFIG_DIR?.trim();
10291
- return config ? `${join8(config, "skills")}/` : "~/.claude/skills/";
11126
+ return config ? `${join11(config, "skills")}/` : "~/.claude/skills/";
10292
11127
  }
10293
11128
  function template(product) {
10294
11129
  return `# firegoat.yaml: evals for how well coding agents use ${product}.
10295
- # Each test runs your claude in an empty folder (.firegoat/runs/...) with \`prompt\`
10296
- # as the first message, then runs \`checks\` there. Reference: npx firegoat init --help
11130
+ # Each test runs your agent (claude by default) in an empty folder
11131
+ # (~/.firegoat/runs/...) with \`prompt\` as the first message, then runs \`checks\`
11132
+ # there. Reference: npx firegoat init --help
10297
11133
 
10298
11134
  # How many times to run each test.
10299
11135
  runs: 1
10300
11136
 
10301
- # Optional: claude's --model. A list runs every test \`runs\` times per model.
10302
- # Unset: claude's default. eval --model NAME overrides it.
11137
+ # Optional: the agent CLI each test runs on: claude-code (default), codex-cli or grok-cli.
11138
+ # A list runs every test \`runs\` times per harness. eval --harness NAME overrides it.
11139
+ # harness: [claude-code, codex-cli]
11140
+
11141
+ # Optional: the harness's --model. A list runs every test \`runs\` times per model.
11142
+ # Unset: the harness's default. eval --model NAME overrides it.
10303
11143
  # model: [claude-haiku-5-5, claude-sonnet-5-5]
10304
11144
 
10305
11145
  # Optional: test your local build instead of the published package.
@@ -10380,7 +11220,7 @@ var CLIPBOARD = [
10380
11220
  ];
10381
11221
  function copyToClipboard(text) {
10382
11222
  return CLIPBOARD.some(([cmd, args]) => {
10383
- const r = spawnSync3(cmd, args, { input: text, env: hostEnv() });
11223
+ const r = spawnSync4(cmd, args, { input: text, env: hostEnv() });
10384
11224
  return !r.error && r.status === 0;
10385
11225
  });
10386
11226
  }
@@ -10545,8 +11385,8 @@ ${INIT_HELP}`);
10545
11385
  console.log(rule);
10546
11386
  }
10547
11387
  ensureFiregoatDir(cwd);
10548
- const configPath = join8(cwd, CONFIG_FILE);
10549
- const keep = existsSync4(configPath) && !force;
11388
+ const configPath = join11(cwd, CONFIG_FILE);
11389
+ const keep = existsSync6(configPath) && !force;
10550
11390
  if (!keep)
10551
11391
  writeFileSync5(configPath, template(basename4(cwd)));
10552
11392
  if (ui) {
@@ -10590,7 +11430,7 @@ ${INIT_HELP}`);
10590
11430
  block.draw([ui.done(3, skill, `Installed → ${where}`)]);
10591
11431
  else
10592
11432
  console.log(`Installed skill firegoat-onboarding in ${where}`);
10593
- const ignored = scope === "local" && spawnSync3("git", ["check-ignore", "-q", ".claude/skills"], {
11433
+ const ignored = scope === "local" && spawnSync4("git", ["check-ignore", "-q", ".claude/skills"], {
10594
11434
  cwd,
10595
11435
  env: hostEnv()
10596
11436
  }).status === 0;