firegoat 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1663 -689
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.25.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -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
|
|
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
|
-
"
|
|
7909
|
+
"harness_args",
|
|
7085
7910
|
"tests"
|
|
7086
7911
|
]);
|
|
7087
7912
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7088
|
-
const
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
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");
|
|
7919
|
+
const build = commands(raw.build, "build", EVAL_PLACEHOLDERS);
|
|
7920
|
+
const install = commands(raw.install, "install", RUN_PLACEHOLDERS);
|
|
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
|
|
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
|
-
"
|
|
7941
|
+
"harness_args"
|
|
7112
7942
|
]);
|
|
7113
|
-
const 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,25 +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
|
-
|
|
7958
|
+
const run = str2(c.run, `${cp}.run`);
|
|
7959
|
+
checkPlaceholders(run, `${cp}.run`, RUN_PLACEHOLDERS);
|
|
7960
|
+
checks.push({ run });
|
|
7129
7961
|
});
|
|
7130
7962
|
return {
|
|
7131
7963
|
name,
|
|
7132
|
-
prompt:
|
|
7133
|
-
end_goal:
|
|
7964
|
+
prompt: str2(t.prompt, `${p}.prompt`),
|
|
7965
|
+
end_goal: str2(t.end_goal, `${p}.end_goal`),
|
|
7134
7966
|
checks,
|
|
7135
7967
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
7136
7968
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
7137
|
-
install: commands(t.install, `${p}.install
|
|
7138
|
-
cleanup: commands(t.cleanup, `${p}.cleanup
|
|
7139
|
-
|
|
7140
|
-
...
|
|
7141
|
-
...
|
|
7969
|
+
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
7970
|
+
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
7971
|
+
harnessArgs: [
|
|
7972
|
+
...harnessArgs,
|
|
7973
|
+
...parseHarnessArgs(t.harness_args, `${p}.harness_args`)
|
|
7142
7974
|
]
|
|
7143
7975
|
};
|
|
7144
7976
|
});
|
|
7145
7977
|
return {
|
|
7146
7978
|
runs,
|
|
7979
|
+
harnesses,
|
|
7147
7980
|
models,
|
|
7148
7981
|
build,
|
|
7149
7982
|
install,
|
|
@@ -7154,67 +7987,53 @@ function parseConfig(text) {
|
|
|
7154
7987
|
};
|
|
7155
7988
|
}
|
|
7156
7989
|
var NAME = /^[A-Za-z0-9._-]+$/;
|
|
7157
|
-
function commands(raw, path) {
|
|
7990
|
+
function commands(raw, path, allowed) {
|
|
7158
7991
|
if (raw === undefined)
|
|
7159
7992
|
return [];
|
|
7160
|
-
if (typeof raw
|
|
7161
|
-
return [str(raw, path)];
|
|
7162
|
-
if (!Array.isArray(raw))
|
|
7993
|
+
if (typeof raw !== "string" && !Array.isArray(raw))
|
|
7163
7994
|
fail(path, "must be a shell command or a list of them");
|
|
7164
|
-
|
|
7995
|
+
const list = typeof raw === "string" ? [raw] : raw;
|
|
7996
|
+
return list.map((c, i) => {
|
|
7997
|
+
const p = typeof raw === "string" ? path : `${path}[${i}]`;
|
|
7998
|
+
const cmd = str2(c, p);
|
|
7999
|
+
checkPlaceholders(cmd, p, allowed);
|
|
8000
|
+
return cmd;
|
|
8001
|
+
});
|
|
7165
8002
|
}
|
|
7166
|
-
function
|
|
7167
|
-
if (raw === undefined)
|
|
7168
|
-
return [];
|
|
8003
|
+
function nameList(raw, key, what) {
|
|
7169
8004
|
if (typeof raw === "string")
|
|
7170
|
-
return [
|
|
8005
|
+
return [str2(raw, key)];
|
|
7171
8006
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
7172
|
-
fail(
|
|
8007
|
+
fail(key, `must be ${what} or a non-empty list of them`);
|
|
7173
8008
|
const seen = new Set;
|
|
7174
8009
|
return raw.map((m, i) => {
|
|
7175
|
-
const name =
|
|
8010
|
+
const name = str2(m, `${key}[${i}]`);
|
|
7176
8011
|
if (seen.has(name))
|
|
7177
|
-
fail(
|
|
8012
|
+
fail(`${key}[${i}]`, `duplicate ${key} ${JSON.stringify(name)}`);
|
|
7178
8013
|
seen.add(name);
|
|
7179
8014
|
return name;
|
|
7180
8015
|
});
|
|
7181
8016
|
}
|
|
7182
|
-
|
|
7183
|
-
[
|
|
7184
|
-
[
|
|
7185
|
-
"-p",
|
|
7186
|
-
"--print",
|
|
7187
|
-
"--output-format",
|
|
7188
|
-
"--verbose",
|
|
7189
|
-
"--setting-sources",
|
|
7190
|
-
"--strict-mcp-config",
|
|
7191
|
-
"--disable-slash-commands",
|
|
7192
|
-
"--dangerously-skip-permissions"
|
|
7193
|
-
],
|
|
7194
|
-
"firegoat sets it for every run"
|
|
7195
|
-
],
|
|
7196
|
-
[["--model"], "choose the model with top-level model: (or eval --model)"],
|
|
7197
|
-
[
|
|
7198
|
-
["--session-id", "--resume", "-r", "--continue", "-c"],
|
|
7199
|
-
"every run is a fresh session"
|
|
7200
|
-
]
|
|
7201
|
-
];
|
|
7202
|
-
function parseClaudeArgs(raw, path) {
|
|
8017
|
+
function parseHarnessArgs(raw, path) {
|
|
7203
8018
|
if (raw === undefined)
|
|
7204
8019
|
return [];
|
|
7205
8020
|
if (!Array.isArray(raw))
|
|
7206
|
-
fail(path, "must be a list of
|
|
8021
|
+
fail(path, "must be a list of harness arguments, one per entry, e.g. [--chrome]");
|
|
7207
8022
|
return raw.map((a, i) => {
|
|
7208
8023
|
const p = `${path}[${i}]`;
|
|
7209
|
-
const arg =
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
if (arg === f || arg.startsWith(`${f}=`))
|
|
7213
|
-
fail(p, `${f} is not allowed: ${why}`);
|
|
7214
|
-
checkPlaceholders(arg, p);
|
|
7215
|
-
return arg;
|
|
8024
|
+
const arg = str2(a, p);
|
|
8025
|
+
checkPlaceholders(arg, p, RUN_PLACEHOLDERS);
|
|
8026
|
+
return { arg, path: p };
|
|
7216
8027
|
});
|
|
7217
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
|
+
}
|
|
7218
8037
|
var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7219
8038
|
function parseVars(raw) {
|
|
7220
8039
|
if (raw === undefined)
|
|
@@ -7234,7 +8053,10 @@ function parseVars(raw) {
|
|
|
7234
8053
|
var RESERVED_ENV = new Set([
|
|
7235
8054
|
"PATH",
|
|
7236
8055
|
"CLAUDE_CONFIG_DIR",
|
|
7237
|
-
"CLAUDE_SECURESTORAGE_CONFIG_DIR"
|
|
8056
|
+
"CLAUDE_SECURESTORAGE_CONFIG_DIR",
|
|
8057
|
+
"CODEX_HOME",
|
|
8058
|
+
"GROK_HOME",
|
|
8059
|
+
"GROK_DISABLE_AUTOUPDATER"
|
|
7238
8060
|
]);
|
|
7239
8061
|
var HOME_ENV = new Set([
|
|
7240
8062
|
"HOME",
|
|
@@ -7264,19 +8086,19 @@ function parseEnv(raw, path) {
|
|
|
7264
8086
|
if (HOME_ENV.has(k))
|
|
7265
8087
|
fail(p, "Reserved: every run gets its own home ($FIREGOAT_HOME)");
|
|
7266
8088
|
if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
|
|
7267
|
-
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_*)");
|
|
7268
8090
|
if (typeof v !== "string")
|
|
7269
8091
|
fail(p, "must be a string (quote numbers and booleans)");
|
|
7270
|
-
checkPlaceholders(v, p);
|
|
8092
|
+
checkPlaceholders(v, p, RUN_PLACEHOLDERS);
|
|
7271
8093
|
out[k] = v;
|
|
7272
8094
|
}
|
|
7273
8095
|
return out;
|
|
7274
8096
|
}
|
|
7275
|
-
function checkPlaceholders(v, p) {
|
|
8097
|
+
function checkPlaceholders(v, p, allowed) {
|
|
7276
8098
|
for (const m of v.matchAll(ANY_PLACEHOLDER)) {
|
|
7277
8099
|
const name = m[1];
|
|
7278
|
-
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !
|
|
7279
|
-
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${
|
|
8100
|
+
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !allowed.includes(name))
|
|
8101
|
+
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${allowed.map((n) => `{{${n}}}`).join(", ")})`);
|
|
7280
8102
|
}
|
|
7281
8103
|
}
|
|
7282
8104
|
function isEvalLevel(value) {
|
|
@@ -7287,13 +8109,16 @@ function isEvalLevel(value) {
|
|
|
7287
8109
|
});
|
|
7288
8110
|
}
|
|
7289
8111
|
function renderValue(value, vars, run, where) {
|
|
7290
|
-
return value.replace(ENV_REF, (whole, name, braced, bare) =>
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
8112
|
+
return value.replace(ENV_REF, (whole, name, braced, bare) => lookup(whole, name ?? braced ?? bare, vars, run, where));
|
|
8113
|
+
}
|
|
8114
|
+
function renderStep(cmd, vars, run, where) {
|
|
8115
|
+
return cmd.replace(ANY_PLACEHOLDER, (whole, name) => lookup(whole, name, vars, run, where));
|
|
8116
|
+
}
|
|
8117
|
+
function lookup(whole, key, vars, run, where) {
|
|
8118
|
+
const filled = key.startsWith("vars.") ? vars[key.slice("vars.".length)] : run[key];
|
|
8119
|
+
if (filled === undefined)
|
|
8120
|
+
throw new ConfigError(`${where}: no value for ${whole}`);
|
|
8121
|
+
return filled;
|
|
7297
8122
|
}
|
|
7298
8123
|
function renderEnv(env, vars, run) {
|
|
7299
8124
|
const out = {};
|
|
@@ -7315,9 +8140,9 @@ function render(text, vars) {
|
|
|
7315
8140
|
}
|
|
7316
8141
|
function loadConfig(dir) {
|
|
7317
8142
|
const file = `${dir}/${CONFIG_FILE}`;
|
|
7318
|
-
if (!
|
|
8143
|
+
if (!existsSync4(file))
|
|
7319
8144
|
throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
|
|
7320
|
-
return parseConfig(
|
|
8145
|
+
return parseConfig(readFileSync3(file, "utf8"));
|
|
7321
8146
|
}
|
|
7322
8147
|
|
|
7323
8148
|
// src/eval.ts
|
|
@@ -7328,24 +8153,24 @@ import {
|
|
|
7328
8153
|
mkdirSync as mkdirSync4,
|
|
7329
8154
|
writeFileSync as writeFileSync4
|
|
7330
8155
|
} from "node:fs";
|
|
7331
|
-
import { basename as basename3, delimiter as delimiter2, join as
|
|
8156
|
+
import { basename as basename3, delimiter as delimiter2, join as join9 } from "node:path";
|
|
7332
8157
|
|
|
7333
8158
|
// src/credentials.ts
|
|
7334
8159
|
import { randomBytes } from "node:crypto";
|
|
7335
8160
|
import {
|
|
7336
8161
|
chmodSync,
|
|
7337
|
-
closeSync,
|
|
8162
|
+
closeSync as closeSync2,
|
|
7338
8163
|
fsyncSync,
|
|
7339
|
-
lstatSync,
|
|
7340
|
-
mkdirSync,
|
|
7341
|
-
openSync,
|
|
7342
|
-
readFileSync as
|
|
8164
|
+
lstatSync as lstatSync3,
|
|
8165
|
+
mkdirSync as mkdirSync2,
|
|
8166
|
+
openSync as openSync2,
|
|
8167
|
+
readFileSync as readFileSync4,
|
|
7343
8168
|
renameSync,
|
|
7344
|
-
unlinkSync,
|
|
8169
|
+
unlinkSync as unlinkSync3,
|
|
7345
8170
|
writeSync
|
|
7346
8171
|
} from "node:fs";
|
|
7347
|
-
import { homedir } from "node:os";
|
|
7348
|
-
import { join } from "node:path";
|
|
8172
|
+
import { homedir as homedir3 } from "node:os";
|
|
8173
|
+
import { join as join5 } from "node:path";
|
|
7349
8174
|
|
|
7350
8175
|
// src/firegoat-api.ts
|
|
7351
8176
|
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
@@ -7497,10 +8322,10 @@ async function whoami(base, key) {
|
|
|
7497
8322
|
// src/credentials.ts
|
|
7498
8323
|
var KEY = /^fire_[\x21-\x7e]+$/;
|
|
7499
8324
|
function credentialsDir() {
|
|
7500
|
-
return
|
|
8325
|
+
return join5(homedir3(), ".firegoat");
|
|
7501
8326
|
}
|
|
7502
8327
|
function credentialsPath() {
|
|
7503
|
-
return
|
|
8328
|
+
return join5(credentialsDir(), "credentials.json");
|
|
7504
8329
|
}
|
|
7505
8330
|
function uid() {
|
|
7506
8331
|
if (!process.getuid)
|
|
@@ -7509,7 +8334,7 @@ function uid() {
|
|
|
7509
8334
|
}
|
|
7510
8335
|
function lstatOrNull(p) {
|
|
7511
8336
|
try {
|
|
7512
|
-
return
|
|
8337
|
+
return lstatSync3(p);
|
|
7513
8338
|
} catch (e) {
|
|
7514
8339
|
if (e.code === "ENOENT")
|
|
7515
8340
|
return null;
|
|
@@ -7539,7 +8364,7 @@ function loadCredentials() {
|
|
|
7539
8364
|
throw new ConfigError(`${path} is readable by others (mode ${(st.mode & 511).toString(8)}): chmod 600 it or run firegoat login again`);
|
|
7540
8365
|
let c;
|
|
7541
8366
|
try {
|
|
7542
|
-
c = JSON.parse(
|
|
8367
|
+
c = JSON.parse(readFileSync4(path, "utf8"));
|
|
7543
8368
|
} catch {
|
|
7544
8369
|
throw new ConfigError(`${path} is not valid JSON: run firegoat logout, then firegoat login`);
|
|
7545
8370
|
}
|
|
@@ -7563,20 +8388,20 @@ function saveCredentials(c) {
|
|
|
7563
8388
|
if (st)
|
|
7564
8389
|
assertOwned(dir, st, "dir");
|
|
7565
8390
|
else
|
|
7566
|
-
|
|
8391
|
+
mkdirSync2(dir, { mode: 448 });
|
|
7567
8392
|
chmodSync(dir, 448);
|
|
7568
8393
|
const tmp = `${credentialsPath()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
7569
|
-
const fd =
|
|
8394
|
+
const fd = openSync2(tmp, "wx", 384);
|
|
7570
8395
|
try {
|
|
7571
8396
|
writeSync(fd, `${JSON.stringify(c, null, 2)}
|
|
7572
8397
|
`);
|
|
7573
8398
|
fsyncSync(fd);
|
|
7574
8399
|
} catch (e) {
|
|
7575
|
-
|
|
7576
|
-
|
|
8400
|
+
closeSync2(fd);
|
|
8401
|
+
unlinkSync3(tmp);
|
|
7577
8402
|
throw e;
|
|
7578
8403
|
}
|
|
7579
|
-
|
|
8404
|
+
closeSync2(fd);
|
|
7580
8405
|
renameSync(tmp, credentialsPath());
|
|
7581
8406
|
}
|
|
7582
8407
|
function removeCredentials() {
|
|
@@ -7597,7 +8422,7 @@ function removeCredentials() {
|
|
|
7597
8422
|
if (!(e instanceof ConfigError))
|
|
7598
8423
|
throw e;
|
|
7599
8424
|
}
|
|
7600
|
-
|
|
8425
|
+
unlinkSync3(path);
|
|
7601
8426
|
return { saved };
|
|
7602
8427
|
}
|
|
7603
8428
|
function resolveKey(base) {
|
|
@@ -7619,15 +8444,15 @@ function resolveKey(base) {
|
|
|
7619
8444
|
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7620
8445
|
|
|
7621
8446
|
// src/firegoat-dir.ts
|
|
7622
|
-
import { mkdirSync as
|
|
7623
|
-
import { join as
|
|
8447
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8448
|
+
import { join as join6 } from "node:path";
|
|
7624
8449
|
var GITIGNORE = `*
|
|
7625
8450
|
`;
|
|
7626
8451
|
function ensureFiregoatDir(root) {
|
|
7627
|
-
const dir =
|
|
7628
|
-
|
|
8452
|
+
const dir = join6(root, ".firegoat");
|
|
8453
|
+
mkdirSync3(dir, { recursive: true });
|
|
7629
8454
|
try {
|
|
7630
|
-
|
|
8455
|
+
writeFileSync2(join6(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
|
|
7631
8456
|
} catch (e) {
|
|
7632
8457
|
if (e.code !== "EEXIST")
|
|
7633
8458
|
throw e;
|
|
@@ -7636,17 +8461,48 @@ function ensureFiregoatDir(root) {
|
|
|
7636
8461
|
}
|
|
7637
8462
|
|
|
7638
8463
|
// src/git.ts
|
|
7639
|
-
import { spawnSync } from "node:child_process";
|
|
8464
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7640
8465
|
import { basename, dirname } from "node:path";
|
|
7641
8466
|
|
|
7642
8467
|
// src/process.ts
|
|
7643
|
-
import { spawn } from "node:child_process";
|
|
8468
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7644
8469
|
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7645
8470
|
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)$/;
|
|
7646
8471
|
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7647
8472
|
function hostEnv() {
|
|
7648
8473
|
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7649
8474
|
}
|
|
8475
|
+
function processTree(pid) {
|
|
8476
|
+
const children = new Map;
|
|
8477
|
+
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
8478
|
+
encoding: "utf8"
|
|
8479
|
+
});
|
|
8480
|
+
for (const line of ps.split(`
|
|
8481
|
+
`)) {
|
|
8482
|
+
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
8483
|
+
if (!child || parent === undefined)
|
|
8484
|
+
continue;
|
|
8485
|
+
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
8486
|
+
}
|
|
8487
|
+
const tree = [pid];
|
|
8488
|
+
for (let i = 0;i < tree.length; i++)
|
|
8489
|
+
tree.push(...children.get(tree[i]) ?? []);
|
|
8490
|
+
return tree;
|
|
8491
|
+
}
|
|
8492
|
+
function killTree(pid) {
|
|
8493
|
+
let tree = [pid];
|
|
8494
|
+
let problem;
|
|
8495
|
+
try {
|
|
8496
|
+
tree = processTree(pid);
|
|
8497
|
+
} catch (e) {
|
|
8498
|
+
problem = e.message;
|
|
8499
|
+
}
|
|
8500
|
+
for (const p of tree)
|
|
8501
|
+
try {
|
|
8502
|
+
process.kill(p, "SIGKILL");
|
|
8503
|
+
} catch {}
|
|
8504
|
+
return problem;
|
|
8505
|
+
}
|
|
7650
8506
|
function run(opts) {
|
|
7651
8507
|
return new Promise((resolve) => {
|
|
7652
8508
|
const child = spawn(opts.cmd, opts.args, {
|
|
@@ -7658,7 +8514,12 @@ function run(opts) {
|
|
|
7658
8514
|
let settled = false;
|
|
7659
8515
|
const timer = setTimeout(() => {
|
|
7660
8516
|
timedOut = true;
|
|
7661
|
-
child.
|
|
8517
|
+
if (child.pid === undefined)
|
|
8518
|
+
return;
|
|
8519
|
+
const problem = killTree(child.pid);
|
|
8520
|
+
if (problem)
|
|
8521
|
+
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
8522
|
+
`));
|
|
7662
8523
|
}, opts.timeoutMs);
|
|
7663
8524
|
const done = (r) => {
|
|
7664
8525
|
if (settled)
|
|
@@ -7676,335 +8537,133 @@ function run(opts) {
|
|
|
7676
8537
|
child.stderr.destroy();
|
|
7677
8538
|
done({ code, timedOut });
|
|
7678
8539
|
}, 2000).unref());
|
|
7679
|
-
});
|
|
7680
|
-
}
|
|
7681
|
-
async function runShell(opts) {
|
|
7682
|
-
opts.log.write(`$ ${opts.run}
|
|
7683
|
-
`);
|
|
7684
|
-
const r = await run({
|
|
7685
|
-
cmd: "/bin/sh",
|
|
7686
|
-
args: ["-c", opts.run],
|
|
7687
|
-
cwd: opts.cwd,
|
|
7688
|
-
env: opts.env,
|
|
7689
|
-
timeoutMs: opts.timeoutMs,
|
|
7690
|
-
onStdout: (c) => opts.log.write(c),
|
|
7691
|
-
onStderr: (c) => opts.log.write(c)
|
|
7692
|
-
});
|
|
7693
|
-
let error;
|
|
7694
|
-
if (r.error)
|
|
7695
|
-
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7696
|
-
else if (r.timedOut)
|
|
7697
|
-
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7698
|
-
else if (r.code === null)
|
|
7699
|
-
error = "killed by a signal";
|
|
7700
|
-
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7701
|
-
|
|
7702
|
-
`);
|
|
7703
|
-
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7704
|
-
}
|
|
7705
|
-
|
|
7706
|
-
// src/git.ts
|
|
7707
|
-
function git(dir, args) {
|
|
7708
|
-
const r = spawnSync("git", args, {
|
|
7709
|
-
cwd: dir,
|
|
7710
|
-
env: hostEnv(),
|
|
7711
|
-
encoding: "utf8"
|
|
7712
|
-
});
|
|
7713
|
-
if (r.error)
|
|
7714
|
-
throw r.error;
|
|
7715
|
-
return { code: r.status ?? -1, stdout: r.stdout.trim() };
|
|
7716
|
-
}
|
|
7717
|
-
function gitRepo(dir) {
|
|
7718
|
-
const r = spawnSync("git", [
|
|
7719
|
-
"rev-parse",
|
|
7720
|
-
"--path-format=absolute",
|
|
7721
|
-
"--git-dir",
|
|
7722
|
-
"--git-common-dir",
|
|
7723
|
-
"--show-toplevel"
|
|
7724
|
-
], { cwd: dir, env: hostEnv(), encoding: "utf8" });
|
|
7725
|
-
if (r.error?.code === "ENOENT")
|
|
7726
|
-
return null;
|
|
7727
|
-
if (r.error)
|
|
7728
|
-
throw r.error;
|
|
7729
|
-
if (r.status !== 0) {
|
|
7730
|
-
if (/not a git repository/.test(r.stderr))
|
|
7731
|
-
return null;
|
|
7732
|
-
throw new Error(`git rev-parse in ${dir} failed: ${r.stderr.trim()}`);
|
|
7733
|
-
}
|
|
7734
|
-
const [gitDir, commonDir, toplevel] = r.stdout.trim().split(`
|
|
7735
|
-
`);
|
|
7736
|
-
if (!gitDir || !commonDir || !toplevel)
|
|
7737
|
-
throw new Error(`git rev-parse in ${dir} printed ${r.stdout}`);
|
|
7738
|
-
let mainCheckout = null;
|
|
7739
|
-
if (gitDir !== commonDir) {
|
|
7740
|
-
if (basename(commonDir) !== ".git")
|
|
7741
|
-
throw new Error(`${dir} is a linked git worktree, but its common git dir ${commonDir} is not a checkout's .git (a bare repo?), so there is no main checkout to keep .firegoat/runs in`);
|
|
7742
|
-
mainCheckout = dirname(commonDir);
|
|
7743
|
-
}
|
|
7744
|
-
const branch = git(dir, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
7745
|
-
const commit = git(dir, ["rev-parse", "-q", "--verify", "HEAD"]);
|
|
7746
|
-
const status = git(dir, ["status", "--porcelain"]);
|
|
7747
|
-
if (status.code !== 0)
|
|
7748
|
-
throw new Error(`git status in ${dir} failed`);
|
|
7749
|
-
return {
|
|
7750
|
-
info: {
|
|
7751
|
-
branch: branch.code === 0 ? branch.stdout : null,
|
|
7752
|
-
commit: commit.code === 0 ? commit.stdout : null,
|
|
7753
|
-
dirty: status.stdout !== ""
|
|
7754
|
-
},
|
|
7755
|
-
mainCheckout,
|
|
7756
|
-
toplevel
|
|
7757
|
-
};
|
|
7758
|
-
}
|
|
7759
|
-
|
|
7760
|
-
// src/harness.ts
|
|
7761
|
-
async function runAgent(harness, opts) {
|
|
7762
|
-
let stdout = "";
|
|
7763
|
-
let pending = "";
|
|
7764
|
-
const exited = await run({
|
|
7765
|
-
cmd: opts.bin,
|
|
7766
|
-
args: harness.args(opts),
|
|
7767
|
-
cwd: opts.cwd,
|
|
7768
|
-
env: opts.env,
|
|
7769
|
-
timeoutMs: opts.timeoutMs,
|
|
7770
|
-
onStdout: (c) => {
|
|
7771
|
-
const text = c.toString();
|
|
7772
|
-
stdout += text;
|
|
7773
|
-
if (!opts.onProgress)
|
|
7774
|
-
return;
|
|
7775
|
-
pending += text;
|
|
7776
|
-
const lines = pending.split(`
|
|
7777
|
-
`);
|
|
7778
|
-
pending = lines.pop() ?? "";
|
|
7779
|
-
for (const line of lines) {
|
|
7780
|
-
const p = harness.progress(line);
|
|
7781
|
-
if (p)
|
|
7782
|
-
opts.onProgress(p);
|
|
7783
|
-
}
|
|
7784
|
-
},
|
|
7785
|
-
onStderr: (c) => opts.stderr.write(c)
|
|
7786
|
-
});
|
|
7787
|
-
if (exited.error)
|
|
7788
|
-
return {
|
|
7789
|
-
ok: false,
|
|
7790
|
-
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
7791
|
-
steps: [],
|
|
7792
|
-
final: "",
|
|
7793
|
-
reportedModel: null,
|
|
7794
|
-
version: null,
|
|
7795
|
-
turns: null,
|
|
7796
|
-
cost: null
|
|
7797
|
-
};
|
|
7798
|
-
return harness.read(stdout, exited, opts.timeoutMs);
|
|
7799
|
-
}
|
|
7800
|
-
|
|
7801
|
-
// src/harnesses/claude.ts
|
|
7802
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7803
|
-
import { existsSync as existsSync2, readdirSync } from "node:fs";
|
|
7804
|
-
import { join as join3 } from "node:path";
|
|
7805
|
-
var FLAGS = [
|
|
7806
|
-
"--output-format",
|
|
7807
|
-
"stream-json",
|
|
7808
|
-
"--verbose",
|
|
7809
|
-
"--dangerously-skip-permissions",
|
|
7810
|
-
"--strict-mcp-config",
|
|
7811
|
-
"--setting-sources",
|
|
7812
|
-
"",
|
|
7813
|
-
"--disable-slash-commands"
|
|
7814
|
-
];
|
|
7815
|
-
function parse(line) {
|
|
7816
|
-
try {
|
|
7817
|
-
const e = JSON.parse(line);
|
|
7818
|
-
return typeof e === "object" && e !== null ? e : undefined;
|
|
7819
|
-
} catch {
|
|
7820
|
-
return;
|
|
7821
|
-
}
|
|
7822
|
-
}
|
|
7823
|
-
function content(e) {
|
|
7824
|
-
const message = e?.message;
|
|
7825
|
-
return Array.isArray(message?.content) ? message.content : [];
|
|
7826
|
-
}
|
|
7827
|
-
function resultText(content) {
|
|
7828
|
-
if (typeof content === "string")
|
|
7829
|
-
return content;
|
|
7830
|
-
if (Array.isArray(content))
|
|
7831
|
-
return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
|
|
7832
|
-
`);
|
|
7833
|
-
return "";
|
|
7834
|
-
}
|
|
7835
|
-
function steps(events) {
|
|
7836
|
-
const out = [];
|
|
7837
|
-
for (const e of events) {
|
|
7838
|
-
if (e?.type === "assistant")
|
|
7839
|
-
for (const b of content(e)) {
|
|
7840
|
-
if (b.type === "tool_use")
|
|
7841
|
-
out.push({
|
|
7842
|
-
kind: "tool_call",
|
|
7843
|
-
name: typeof b.name === "string" ? b.name : "",
|
|
7844
|
-
input: typeof b.input === "object" && b.input !== null ? b.input : null
|
|
7845
|
-
});
|
|
7846
|
-
else if (b.type === "text" && typeof b.text === "string")
|
|
7847
|
-
out.push({ kind: "text", text: b.text });
|
|
7848
|
-
}
|
|
7849
|
-
else if (e?.type === "user") {
|
|
7850
|
-
for (const b of content(e))
|
|
7851
|
-
if (b.type === "tool_result")
|
|
7852
|
-
out.push({
|
|
7853
|
-
kind: "tool_result",
|
|
7854
|
-
isError: b.is_error === true,
|
|
7855
|
-
text: resultText(b.content).trimEnd()
|
|
7856
|
-
});
|
|
7857
|
-
}
|
|
7858
|
-
}
|
|
7859
|
-
return out;
|
|
7860
|
-
}
|
|
7861
|
-
function read(stdout, exited, timeoutMs) {
|
|
7862
|
-
const events = stdout.split(`
|
|
7863
|
-
`).filter((l) => l.trim() !== "").map(parse);
|
|
7864
|
-
let final = "";
|
|
7865
|
-
for (const e of events)
|
|
7866
|
-
if (e?.type === "result" && typeof e.result === "string")
|
|
7867
|
-
final = e.result;
|
|
7868
|
-
const result = events.findLast((e) => e?.type === "result");
|
|
7869
|
-
const trace = {
|
|
7870
|
-
steps: steps(events),
|
|
7871
|
-
final,
|
|
7872
|
-
turns: result?.num_turns ?? null,
|
|
7873
|
-
cost: result?.total_cost_usd ?? null
|
|
7874
|
-
};
|
|
7875
|
-
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7876
|
-
if (!init) {
|
|
7877
|
-
const said = stdout.trim().split(`
|
|
7878
|
-
`)[0]?.slice(0, 300);
|
|
7879
|
-
return {
|
|
7880
|
-
...trace,
|
|
7881
|
-
reportedModel: null,
|
|
7882
|
-
version: null,
|
|
7883
|
-
ok: false,
|
|
7884
|
-
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7885
|
-
};
|
|
7886
|
-
}
|
|
7887
|
-
const reportedModel = typeof init.model === "string" ? init.model : null;
|
|
7888
|
-
const version = typeof init.claude_code_version === "string" ? init.claude_code_version : null;
|
|
7889
|
-
if (typeof init.session_id !== "string" || init.session_id === "")
|
|
7890
|
-
return {
|
|
7891
|
-
...trace,
|
|
7892
|
-
reportedModel,
|
|
7893
|
-
version,
|
|
7894
|
-
ok: false,
|
|
7895
|
-
reason: "claude's init line has no session_id"
|
|
7896
|
-
};
|
|
7897
|
-
const sessionId = init.session_id;
|
|
7898
|
-
if (exited.timedOut)
|
|
7899
|
-
return {
|
|
7900
|
-
...trace,
|
|
7901
|
-
reportedModel,
|
|
7902
|
-
version,
|
|
7903
|
-
ok: false,
|
|
7904
|
-
reason: `timed out after ${timeoutMs / 1000}s`,
|
|
7905
|
-
sessionId
|
|
7906
|
-
};
|
|
7907
|
-
if (!result)
|
|
7908
|
-
return {
|
|
7909
|
-
...trace,
|
|
7910
|
-
reportedModel,
|
|
7911
|
-
version,
|
|
7912
|
-
ok: false,
|
|
7913
|
-
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7914
|
-
sessionId
|
|
7915
|
-
};
|
|
7916
|
-
if (result.is_error === true || result.subtype !== "success")
|
|
7917
|
-
return {
|
|
7918
|
-
...trace,
|
|
7919
|
-
reportedModel,
|
|
7920
|
-
version,
|
|
7921
|
-
ok: false,
|
|
7922
|
-
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7923
|
-
sessionId
|
|
7924
|
-
};
|
|
7925
|
-
return { ...trace, reportedModel, version, ok: true, sessionId };
|
|
8540
|
+
});
|
|
7926
8541
|
}
|
|
7927
|
-
function
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
8542
|
+
async function runShell(opts) {
|
|
8543
|
+
opts.log.write(`$ ${opts.run}
|
|
8544
|
+
`);
|
|
8545
|
+
const r = await run({
|
|
8546
|
+
cmd: "/bin/sh",
|
|
8547
|
+
args: ["-c", opts.run],
|
|
8548
|
+
cwd: opts.cwd,
|
|
8549
|
+
env: opts.env,
|
|
8550
|
+
timeoutMs: opts.timeoutMs,
|
|
8551
|
+
onStdout: (c) => opts.log.write(c),
|
|
8552
|
+
onStderr: (c) => opts.log.write(c)
|
|
8553
|
+
});
|
|
8554
|
+
let error;
|
|
8555
|
+
if (r.error)
|
|
8556
|
+
error = `could not start /bin/sh: ${r.error.message}`;
|
|
8557
|
+
else if (r.timedOut)
|
|
8558
|
+
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
8559
|
+
else if (r.code === null)
|
|
8560
|
+
error = "killed by a signal";
|
|
8561
|
+
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
8562
|
+
|
|
8563
|
+
`);
|
|
8564
|
+
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
8565
|
+
}
|
|
8566
|
+
|
|
8567
|
+
// src/git.ts
|
|
8568
|
+
function git(dir, args) {
|
|
8569
|
+
const r = spawnSync3("git", args, {
|
|
8570
|
+
cwd: dir,
|
|
8571
|
+
env: hostEnv(),
|
|
8572
|
+
encoding: "utf8"
|
|
8573
|
+
});
|
|
8574
|
+
if (r.error)
|
|
8575
|
+
throw r.error;
|
|
8576
|
+
return { code: r.status ?? -1, stdout: r.stdout.trim() };
|
|
8577
|
+
}
|
|
8578
|
+
function gitRepo(dir) {
|
|
8579
|
+
const r = spawnSync3("git", [
|
|
8580
|
+
"rev-parse",
|
|
8581
|
+
"--path-format=absolute",
|
|
8582
|
+
"--git-dir",
|
|
8583
|
+
"--git-common-dir",
|
|
8584
|
+
"--show-toplevel"
|
|
8585
|
+
], { cwd: dir, env: hostEnv(), encoding: "utf8" });
|
|
8586
|
+
if (r.error?.code === "ENOENT")
|
|
8587
|
+
return null;
|
|
8588
|
+
if (r.error)
|
|
8589
|
+
throw r.error;
|
|
8590
|
+
if (r.status !== 0) {
|
|
8591
|
+
if (/not a git repository/.test(r.stderr))
|
|
8592
|
+
return null;
|
|
8593
|
+
throw new Error(`git rev-parse in ${dir} failed: ${r.stderr.trim()}`);
|
|
7937
8594
|
}
|
|
7938
|
-
|
|
8595
|
+
const [gitDir, commonDir, toplevel] = r.stdout.trim().split(`
|
|
8596
|
+
`);
|
|
8597
|
+
if (!gitDir || !commonDir || !toplevel)
|
|
8598
|
+
throw new Error(`git rev-parse in ${dir} printed ${r.stdout}`);
|
|
8599
|
+
let mainCheckout = null;
|
|
8600
|
+
if (gitDir !== commonDir) {
|
|
8601
|
+
if (basename(commonDir) !== ".git")
|
|
8602
|
+
throw new Error(`${dir} is a linked git worktree, but its common git dir ${commonDir} is not a checkout's .git (a bare repo?), so there is no main checkout to keep .firegoat/runs in`);
|
|
8603
|
+
mainCheckout = dirname(commonDir);
|
|
8604
|
+
}
|
|
8605
|
+
const branch = git(dir, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
8606
|
+
const commit = git(dir, ["rev-parse", "-q", "--verify", "HEAD"]);
|
|
8607
|
+
const status = git(dir, ["status", "--porcelain"]);
|
|
8608
|
+
if (status.code !== 0)
|
|
8609
|
+
throw new Error(`git status in ${dir} failed`);
|
|
8610
|
+
return {
|
|
8611
|
+
info: {
|
|
8612
|
+
branch: branch.code === 0 ? branch.stdout : null,
|
|
8613
|
+
commit: commit.code === 0 ? commit.stdout : null,
|
|
8614
|
+
dirty: status.stdout !== ""
|
|
8615
|
+
},
|
|
8616
|
+
mainCheckout,
|
|
8617
|
+
toplevel
|
|
8618
|
+
};
|
|
7939
8619
|
}
|
|
7940
|
-
|
|
7941
|
-
|
|
8620
|
+
|
|
8621
|
+
// src/harness.ts
|
|
8622
|
+
async function runAgent(harness, opts) {
|
|
8623
|
+
let stdout = "";
|
|
8624
|
+
let pending = "";
|
|
8625
|
+
const exited = await run({
|
|
8626
|
+
cmd: opts.bin,
|
|
8627
|
+
args: harness.args(opts),
|
|
7942
8628
|
cwd: opts.cwd,
|
|
7943
8629
|
env: opts.env,
|
|
7944
|
-
|
|
7945
|
-
|
|
8630
|
+
timeoutMs: opts.timeoutMs,
|
|
8631
|
+
onStdout: (c) => {
|
|
8632
|
+
const text = c.toString();
|
|
8633
|
+
stdout += text;
|
|
8634
|
+
if (!opts.onProgress)
|
|
8635
|
+
return;
|
|
8636
|
+
pending += text;
|
|
8637
|
+
const lines = pending.split(`
|
|
8638
|
+
`);
|
|
8639
|
+
pending = lines.pop() ?? "";
|
|
8640
|
+
for (const line of lines) {
|
|
8641
|
+
const p = harness.progress(line);
|
|
8642
|
+
if (p)
|
|
8643
|
+
opts.onProgress(p);
|
|
8644
|
+
}
|
|
8645
|
+
},
|
|
8646
|
+
onStderr: (c) => opts.stderr.write(c)
|
|
7946
8647
|
});
|
|
7947
|
-
if (
|
|
7948
|
-
return {
|
|
7949
|
-
loggedOut: false,
|
|
7950
|
-
reason: `could not run ${opts.bin}: ${r.error.message}`
|
|
7951
|
-
};
|
|
7952
|
-
let status;
|
|
7953
|
-
try {
|
|
7954
|
-
status = JSON.parse(r.stdout);
|
|
7955
|
-
} catch {}
|
|
7956
|
-
if (status?.loggedIn === true)
|
|
7957
|
-
return;
|
|
7958
|
-
if (status?.loggedIn === false)
|
|
8648
|
+
if (exited.error)
|
|
7959
8649
|
return {
|
|
7960
|
-
|
|
7961
|
-
reason:
|
|
8650
|
+
ok: false,
|
|
8651
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
8652
|
+
steps: [],
|
|
8653
|
+
final: "",
|
|
8654
|
+
reportedModel: null,
|
|
8655
|
+
version: null,
|
|
8656
|
+
turns: null,
|
|
8657
|
+
cost: null
|
|
7962
8658
|
};
|
|
7963
|
-
return
|
|
7964
|
-
loggedOut: false,
|
|
7965
|
-
reason: `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`
|
|
7966
|
-
};
|
|
8659
|
+
return harness.read(stdout, exited, opts.timeoutMs);
|
|
7967
8660
|
}
|
|
7968
|
-
var claude = {
|
|
7969
|
-
name: "claude",
|
|
7970
|
-
bin: "claude",
|
|
7971
|
-
installHint: "Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH.",
|
|
7972
|
-
configDirName: ".claude",
|
|
7973
|
-
args: ({ prompt, model, extraArgs }) => [
|
|
7974
|
-
"-p",
|
|
7975
|
-
prompt,
|
|
7976
|
-
...FLAGS,
|
|
7977
|
-
...model === undefined ? [] : ["--model", model],
|
|
7978
|
-
...extraArgs
|
|
7979
|
-
],
|
|
7980
|
-
env: ({ configDir, homeEnvFile }) => ({
|
|
7981
|
-
CLAUDE_CONFIG_DIR: configDir,
|
|
7982
|
-
CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
|
|
7983
|
-
...homeEnvFile === undefined ? {} : { CLAUDE_ENV_FILE: homeEnvFile }
|
|
7984
|
-
}),
|
|
7985
|
-
authLabel: () => process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login",
|
|
7986
|
-
checkLogin,
|
|
7987
|
-
progress,
|
|
7988
|
-
read,
|
|
7989
|
-
sessionsDir: (configDir) => join3(configDir, "projects"),
|
|
7990
|
-
findSession(configDir, sessionId) {
|
|
7991
|
-
const projects = join3(configDir, "projects");
|
|
7992
|
-
if (!existsSync2(projects))
|
|
7993
|
-
return null;
|
|
7994
|
-
for (const d of readdirSync(projects)) {
|
|
7995
|
-
const f = join3(projects, d, `${sessionId}.jsonl`);
|
|
7996
|
-
if (existsSync2(f))
|
|
7997
|
-
return f;
|
|
7998
|
-
}
|
|
7999
|
-
return null;
|
|
8000
|
-
}
|
|
8001
|
-
};
|
|
8002
8661
|
|
|
8003
8662
|
// src/help.ts
|
|
8004
8663
|
var EXIT_CODES = `Exit codes:
|
|
8005
8664
|
0 Every run's script checks passed
|
|
8006
8665
|
1 A script check failed
|
|
8007
|
-
2 At least one run was not graded (
|
|
8666
|
+
2 At least one run was not graded (the harness missing or logged out, crash, timeout,
|
|
8008
8667
|
build failed); wins over 1
|
|
8009
8668
|
3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
|
|
8010
8669
|
firegoat.ai answered 401/403 for the upload key); any other failure of
|
|
@@ -8058,10 +8717,18 @@ firegoat.yaml, committed.
|
|
|
8058
8717
|
|
|
8059
8718
|
firegoat.yaml reference:
|
|
8060
8719
|
runs: 1 Runs per test
|
|
8720
|
+
harness: <name> | [<name>, ...]
|
|
8721
|
+
The agent CLI: claude-code (default), codex-cli
|
|
8722
|
+
or grok-cli.
|
|
8723
|
+
A list runs every test runs times per harness;
|
|
8724
|
+
eval --harness overrides
|
|
8061
8725
|
model: <name> | [<name>, ...]
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8726
|
+
The harness's --model (-m for codex, grok); a list
|
|
8727
|
+
runs
|
|
8728
|
+
every test runs times per model, on each harness
|
|
8729
|
+
(n counts 1..runs x harnesses x models, harness by
|
|
8730
|
+
harness, then model). Unset: no --model, the
|
|
8731
|
+
harness's default. eval --model overrides
|
|
8065
8732
|
build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
|
|
8066
8733
|
$FIREGOAT_OUT is first on the agent's PATH
|
|
8067
8734
|
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
|
|
@@ -8071,8 +8738,9 @@ firegoat.yaml reference:
|
|
|
8071
8738
|
what the agent left running (servers, daemons).
|
|
8072
8739
|
Every step runs; a failure is a warning (run.json
|
|
8073
8740
|
cleanup, <test>/<n>/cleanup.txt), never the verdict
|
|
8074
|
-
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts and
|
|
8075
|
-
--var KEY=value
|
|
8741
|
+
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts, checks and
|
|
8742
|
+
build, install and cleanup steps; --var KEY=value
|
|
8743
|
+
overrides. A missing value is a config error
|
|
8076
8744
|
env: {NAME: value} Env vars for the agent, and for install, checks
|
|
8077
8745
|
and cleanup of the same run; values are strings and can
|
|
8078
8746
|
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
@@ -8080,7 +8748,8 @@ firegoat.yaml reference:
|
|
|
8080
8748
|
$FIREGOAT_HOME (no other $ expansion: $HOME stays
|
|
8081
8749
|
literal). The build step gets only the entries that
|
|
8082
8750
|
use only {{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
|
|
8083
|
-
PATH, CLAUDE_CONFIG_DIR,
|
|
8751
|
+
PATH, CLAUDE_CONFIG_DIR, CODEX_HOME, GROK_HOME,
|
|
8752
|
+
GROK_DISABLE_AUTOUPDATER, FIREGOAT_* and
|
|
8084
8753
|
ANTHROPIC_MODEL (use model:) are reserved, and so
|
|
8085
8754
|
are HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME,
|
|
8086
8755
|
XDG_DATA_HOME, XDG_STATE_HOME and CLAUDE_ENV_FILE:
|
|
@@ -8088,20 +8757,33 @@ firegoat.yaml reference:
|
|
|
8088
8757
|
tool that keeps state in ~ starts fresh in each
|
|
8089
8758
|
run. Only a tool that ignores HOME needs its own
|
|
8090
8759
|
var, e.g. MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
8091
|
-
|
|
8092
|
-
and --model
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
[--
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8760
|
+
harness_args: [<arg>, ...] Extra harness arguments, after firegoat's own flags
|
|
8761
|
+
and --model (for codex, before the prompt), to
|
|
8762
|
+
every selected harness. Each entry is one
|
|
8763
|
+
argument, passed as-is (no shell splitting):
|
|
8764
|
+
claude: [--chrome] (Claude in Chrome, for tests
|
|
8765
|
+
that need a browser), [--plugin-dir, ./plugins/x],
|
|
8766
|
+
--mcp-config, --agents, --append-system-prompt;
|
|
8767
|
+
codex: [-c, key=value], --enable, --add-dir.
|
|
8768
|
+
Entries expand like env: values
|
|
8769
|
+
($FIREGOAT_WORKDIR, $FIREGOAT_HOME, $FIREGOAT_OUT,
|
|
8770
|
+
{{vars.KEY}}, {{run.*}}); run.json harnessArgs has
|
|
8771
|
+
the result. Flags firegoat sets are errors, also
|
|
8772
|
+
as --flag=value. claude-code: -p, --print,
|
|
8101
8773
|
--output-format, --verbose, --setting-sources,
|
|
8102
8774
|
--strict-mcp-config, --disable-slash-commands,
|
|
8103
8775
|
--dangerously-skip-permissions, --model (use
|
|
8104
|
-
model:), --session-id, --resume, -r, --continue,
|
|
8776
|
+
model:), --session-id, --resume, -r, --continue,
|
|
8777
|
+
-c. codex-cli: --json, --skip-git-repo-check,
|
|
8778
|
+
--dangerously-bypass-approvals-and-sandbox, -C,
|
|
8779
|
+
--cd, -m, --model (use model:), resume, fork,
|
|
8780
|
+
--ephemeral. grok-cli: -p, --single,
|
|
8781
|
+
--prompt-file, --prompt-json, --output-format,
|
|
8782
|
+
--json-schema, --always-approve,
|
|
8783
|
+
--permission-mode, --no-auto-update, --cwd, -w,
|
|
8784
|
+
--worktree, -m, --model (use model:), -r,
|
|
8785
|
+
--resume, -c, --continue, -s, --session-id,
|
|
8786
|
+
--fork-session
|
|
8105
8787
|
tests:
|
|
8106
8788
|
- name: quickstart Letters, digits, '.', '_', '-'; unique
|
|
8107
8789
|
prompt: ... What a user would ask the agent; one concrete outcome
|
|
@@ -8113,7 +8795,7 @@ firegoat.yaml reference:
|
|
|
8113
8795
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
8114
8796
|
install: <sh> | [...] Optional, runs after the top-level install
|
|
8115
8797
|
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
8116
|
-
|
|
8798
|
+
harness_args: [...] Optional, after the top-level harness_args
|
|
8117
8799
|
|
|
8118
8800
|
Step order: build once per eval; then per run: install (top-level, then the
|
|
8119
8801
|
test's), the agent, checks, cleanup (the test's, then top-level).
|
|
@@ -8123,13 +8805,18 @@ firegoat.yaml reference:
|
|
|
8123
8805
|
from a template project, commit it as e.g. fixtures/starter-app/, then:
|
|
8124
8806
|
build: cp -R fixtures/starter-app "$FIREGOAT_OUT/starter-app"
|
|
8125
8807
|
install: cp -R "$FIREGOAT_OUT/starter-app/." .
|
|
8808
|
+
Placeholders in build, install, cleanup and checks: {{vars.KEY}}, {{run.id}},
|
|
8809
|
+
{{run.n}}, {{test.name}} (build: only {{vars.KEY}} and {{run.id}}); others are
|
|
8810
|
+
config errors. Values are pasted into the shell text as-is, unquoted: quote
|
|
8811
|
+
them yourself ('{{vars.KEY}}'), or use the env var below ("$KEY"), which is safe
|
|
8812
|
+
for any value.
|
|
8126
8813
|
The build, install and cleanup commands and the checks get the vars as env
|
|
8127
8814
|
vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
|
|
8128
8815
|
get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME).
|
|
8129
8816
|
Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
8130
8817
|
transcript, greppable line by line (format: eval --help, Transcript). So
|
|
8131
8818
|
'^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
|
|
8132
|
-
$FIREGOAT_SESSION (
|
|
8819
|
+
$FIREGOAT_SESSION (the harness's full session file, JSON lines).
|
|
8133
8820
|
|
|
8134
8821
|
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
8135
8822
|
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
@@ -8140,7 +8827,7 @@ firegoat.yaml reference:
|
|
|
8140
8827
|
so the agent's commands see them too.
|
|
8141
8828
|
|
|
8142
8829
|
Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
|
|
8143
|
-
and pass it with
|
|
8830
|
+
and pass it with harness_args (claude-code). Only the configs you pass load: --strict-mcp-config
|
|
8144
8831
|
is always on. An MCP server inherits claude's env, which keeps your real HOME
|
|
8145
8832
|
(claude's login is there), so set the server's HOME to the run's home, or it
|
|
8146
8833
|
writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
|
|
@@ -8151,7 +8838,7 @@ relative paths in it are from the run's folder, claude's cwd:
|
|
|
8151
8838
|
"env": {"HOME": "\${FIREGOAT_HOME}"}}}}
|
|
8152
8839
|
firegoat.yaml:
|
|
8153
8840
|
build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
8154
|
-
|
|
8841
|
+
harness_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
8155
8842
|
|
|
8156
8843
|
Example:
|
|
8157
8844
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -8175,7 +8862,7 @@ Example:
|
|
|
8175
8862
|
Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put
|
|
8176
8863
|
it in .env as ACME_API_KEY.
|
|
8177
8864
|
end_goal: .env holds a key copied from the dashboard, not a made-up one.
|
|
8178
|
-
|
|
8865
|
+
harness_args: [--chrome]
|
|
8179
8866
|
|
|
8180
8867
|
init also says that firegoat sends anonymous usage stats; FIREGOAT_TELEMETRY=0
|
|
8181
8868
|
(or DO_NOT_TRACK=1) turns them off. Fields: firegoat --help, Telemetry.
|
|
@@ -8220,9 +8907,10 @@ Exit codes: 0 logged out (or was not logged in), 3 the file or ~/.firegoat is a
|
|
|
8220
8907
|
symlink, not a regular file/directory, or someone else's (left alone).`;
|
|
8221
8908
|
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--parallel N]
|
|
8222
8909
|
[--var KEY=value].. [--json] [--no-upload] [--dry-run]
|
|
8223
|
-
[--
|
|
8910
|
+
[--harness NAME].. [--harness-bin PATH]
|
|
8224
8911
|
|
|
8225
|
-
Runs each test in firegoat.yaml (in the current folder): your
|
|
8912
|
+
Runs each test in firegoat.yaml (in the current folder): your agent harness
|
|
8913
|
+
(claude-code by default, codex-cli or grok-cli; harness: in firegoat.yaml) gets the prompt
|
|
8226
8914
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
|
|
8227
8915
|
outputs on PATH. Then the test's checks run in that folder.
|
|
8228
8916
|
|
|
@@ -8234,10 +8922,32 @@ changes the run's status or the exit code.
|
|
|
8234
8922
|
|
|
8235
8923
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
8236
8924
|
with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
|
|
8237
|
-
plugins, skills or MCP servers (
|
|
8925
|
+
plugins, skills or MCP servers (harness_args: in firegoat.yaml adds claude flags,
|
|
8238
8926
|
e.g. --chrome or --plugin-dir). claude saves its session there. There is no
|
|
8239
8927
|
sandbox: the agent can reach the rest of your machine, including the repo.
|
|
8240
8928
|
|
|
8929
|
+
codex-cli runs codex exec --json --dangerously-bypass-approvals-and-sandbox with a
|
|
8930
|
+
fresh CODEX_HOME, .firegoat/.codex, holding a link to your codex login
|
|
8931
|
+
(~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
|
|
8932
|
+
-c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
|
|
8933
|
+
yours. codex's own HOME is the run's home (below), also for its Read/Write. It
|
|
8934
|
+
saves its session (rollout) under .firegoat/.codex/sessions/. firegoat.ai doesn't
|
|
8935
|
+
read codex sessions yet: those runs are not uploaded. codex reports no turns or
|
|
8936
|
+
cost; its model comes from the session file.
|
|
8937
|
+
|
|
8938
|
+
grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
|
|
8939
|
+
PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
|
|
8940
|
+
--output-format streaming-messages-json --always-approve --no-auto-update, with a
|
|
8941
|
+
fresh GROK_HOME, .firegoat/.grok, holding a link to your grok login
|
|
8942
|
+
($GROK_HOME/auth.json or ~/.grok/auth.json; run grok login first; XAI_API_KEY alone
|
|
8943
|
+
does not log headless grok in). grok's own HOME is the run's home (below): with
|
|
8944
|
+
your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eval
|
|
8945
|
+
refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
|
|
8946
|
+
a refresh inside a run would replace the link with a file, which the next eval
|
|
8947
|
+
asks you to move back over ~/.grok/auth.json. The session is a directory,
|
|
8948
|
+
.firegoat/.grok/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). firegoat.ai doesn't read
|
|
8949
|
+
grok sessions yet: those runs are not uploaded.
|
|
8950
|
+
|
|
8241
8951
|
Every run gets its own HOME, .firegoat/runs/<runId>/<test>/<n>/home (with
|
|
8242
8952
|
XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
|
|
8243
8953
|
$FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
|
|
@@ -8258,35 +8968,47 @@ nested session. Login and provider vars stay: ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOK
|
|
|
8258
8968
|
CLAUDE_CODE_OAUTH_REFRESH_TOKEN, CLAUDE_CODE_USE_*, CLAUDE_CODE_SKIP_*_AUTH,
|
|
8259
8969
|
CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, CLAUDE_CODE_CLIENT_KEY_PASSPHRASE.
|
|
8260
8970
|
|
|
8261
|
-
In a linked git worktree (git worktree add), .firegoat/runs/ and
|
|
8262
|
-
live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
|
|
8971
|
+
In a linked git worktree (git worktree add), .firegoat/runs/ and the harness
|
|
8972
|
+
config dirs (.firegoat/.claude, .firegoat/.codex, .firegoat/.grok) live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
|
|
8263
8973
|
fixtures and build: stay in the current folder.
|
|
8264
8974
|
|
|
8265
8975
|
test.. Only these tests (by name)
|
|
8266
8976
|
--runs N Runs per test (overrides runs: in firegoat.yaml)
|
|
8267
|
-
--
|
|
8268
|
-
|
|
8977
|
+
--harness NAME claude-code, codex-cli or grok-cli, replacing harness: in
|
|
8978
|
+
firegoat.yaml;
|
|
8979
|
+
repeat it to run each test on each harness
|
|
8980
|
+
--model NAME The harness's --model, replacing model: in firegoat.yaml;
|
|
8981
|
+
repeat it (--model a --model b) to run each test runs times
|
|
8982
|
+
per model
|
|
8269
8983
|
--parallel N Runs at once (default 4; 1 runs them one after another).
|
|
8270
8984
|
Each run has its own HOME; state outside it (a fixed port,
|
|
8271
8985
|
/tmp paths) can still collide: use 1 then
|
|
8272
|
-
--var KEY=value Fills {{vars.KEY}}
|
|
8273
|
-
cleanup
|
|
8986
|
+
--var KEY=value Fills {{vars.KEY}} (prompts, checks, build, install,
|
|
8987
|
+
cleanup); env for build, install, checks and cleanup
|
|
8274
8988
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
8275
|
-
stops early); progress goes to stderr
|
|
8989
|
+
stops early); progress goes to stderr. The verdict comes
|
|
8990
|
+
last, after runs: totals {pass, fail, error}, status
|
|
8991
|
+
("pass", "fail" or "error" = not graded) and exitCode
|
|
8276
8992
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
8277
8993
|
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
8278
|
-
--model, missing vars), print each planned
|
|
8279
|
-
("<test>#<n> [model]" with its install, checks
|
|
8280
|
-
counts and rendered
|
|
8994
|
+
--model, --harness, missing vars), print each planned
|
|
8995
|
+
run ("<test>#<n> [harness model]" with its install, checks
|
|
8996
|
+
and cleanup counts and rendered harness_args, paths under
|
|
8281
8997
|
runs/<runId>/) and exit 0. Builds nothing, starts no
|
|
8282
|
-
|
|
8998
|
+
agent, uploads nothing, writes no files. Config errors
|
|
8283
8999
|
exit 3. With --json: {dryRun, build, runs: [{test, n,
|
|
8284
|
-
model,
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
9000
|
+
harness, model, harnessArgs, install, checks,
|
|
9001
|
+
cleanup}]}, steps and checks with their placeholders
|
|
9002
|
+
filled
|
|
9003
|
+
--harness-bin PATH Path to the harness binary (default: claude or codex on
|
|
9004
|
+
PATH, ~/.grok/bin/grok); only with exactly one harness
|
|
9005
|
+
|
|
9006
|
+
Run labels: "PASS <test>#<n> [<harness> <model>]": the harness shows when the eval
|
|
9007
|
+
runs more than one, or not claude-code; the model when one is set.
|
|
9008
|
+
|
|
9009
|
+
Upload: after firegoat login (or with FIREGOAT_API_KEY), each claude-code run's
|
|
9010
|
+
session, checks.txt and stderr.txt go to firegoat.ai as a private trace, and its
|
|
9011
|
+
URL is printed ("✓ <test>#<n> <url>", or "✗ ... Upload failed: <why>") and saved as
|
|
8290
9012
|
upload in run.json. Before any run, eval checks the key: a 401/403 exits 3, any
|
|
8291
9013
|
other failure (unreachable, 429, 5xx, wrong FIREGOAT_BASE_URL) exits 2 (fix it, or
|
|
8292
9014
|
--no-upload). A failed upload never changes a run's status or the exit code; eval
|
|
@@ -8295,19 +9017,21 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
|
8295
9017
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
8296
9018
|
|
|
8297
9019
|
Output: .firegoat/runs/<runId>/
|
|
8298
|
-
summary.json
|
|
8299
|
-
git {branch, commit, dirty} (no git outside a
|
|
8300
|
-
|
|
9020
|
+
summary.json firegoatVersion, runId, runsDir, dir (where eval
|
|
9021
|
+
ran), git {branch, commit, dirty} (no git outside a
|
|
9022
|
+
repo), harnesses, upload {origin, email} (null when
|
|
9023
|
+
not uploading), runs (every run.json), then the
|
|
9024
|
+
verdict: totals, status and exitCode
|
|
8301
9025
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
8302
|
-
commit, passed, failed, notGraded, cost (null when
|
|
9026
|
+
commit, harnesses, passed, failed, notGraded, cost (null when
|
|
8303
9027
|
any run's cost is unknown), uploaded, uploadFailed
|
|
8304
|
-
<test>/<n>/run.json One run: status and reason (from the checks),
|
|
8305
|
-
(asked for, or null) and
|
|
8306
|
-
|
|
8307
|
-
passed), prompt, end_goal,
|
|
9028
|
+
<test>/<n>/run.json One run: status and reason (from the checks), harness,
|
|
9029
|
+
model (asked for, or null) and reportedModel (what
|
|
9030
|
+
the harness reported running, or null), harnessArgs
|
|
9031
|
+
(harness_args passed), prompt, end_goal,
|
|
8308
9032
|
checks [{run, code}], cleanup [{run, code}],
|
|
8309
9033
|
and paths: workdir, home (the run's HOME),
|
|
8310
|
-
transcript, session (
|
|
9034
|
+
transcript, session (the harness's session file), stderr,
|
|
8311
9035
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
8312
9036
|
url} | {status: "failed", error} | {status:
|
|
8313
9037
|
"skipped", reason}, null when not uploading)
|
|
@@ -8339,6 +9063,8 @@ Examples:
|
|
|
8339
9063
|
npx firegoat eval quickstart --runs 3 One test, three runs
|
|
8340
9064
|
npx firegoat eval --model claude-haiku-5-5 --model claude-sonnet-5-5
|
|
8341
9065
|
Each test on both models
|
|
9066
|
+
npx firegoat eval --harness claude-code --harness codex-cli
|
|
9067
|
+
Each test on claude and on codex
|
|
8342
9068
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
8343
9069
|
npx firegoat eval --dry-run Validate firegoat.yaml, list the runs
|
|
8344
9070
|
|
|
@@ -8367,30 +9093,6 @@ Example:
|
|
|
8367
9093
|
|
|
8368
9094
|
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
8369
9095
|
|
|
8370
|
-
// src/home.ts
|
|
8371
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8372
|
-
import { join as join4 } from "node:path";
|
|
8373
|
-
function homeEnv(home) {
|
|
8374
|
-
return {
|
|
8375
|
-
HOME: home,
|
|
8376
|
-
XDG_CONFIG_HOME: join4(home, ".config"),
|
|
8377
|
-
XDG_CACHE_HOME: join4(home, ".cache"),
|
|
8378
|
-
XDG_DATA_HOME: join4(home, ".local", "share"),
|
|
8379
|
-
XDG_STATE_HOME: join4(home, ".local", "state")
|
|
8380
|
-
};
|
|
8381
|
-
}
|
|
8382
|
-
var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
8383
|
-
function makeRunHome(dir) {
|
|
8384
|
-
const home = join4(dir, "home");
|
|
8385
|
-
const env = homeEnv(home);
|
|
8386
|
-
for (const d of Object.values(env))
|
|
8387
|
-
mkdirSync3(d, { recursive: true });
|
|
8388
|
-
const envFile = join4(dir, "home.env");
|
|
8389
|
-
writeFileSync2(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
|
|
8390
|
-
`).join(""));
|
|
8391
|
-
return { home, envFile };
|
|
8392
|
-
}
|
|
8393
|
-
|
|
8394
9096
|
// src/steps.ts
|
|
8395
9097
|
import { createWriteStream } from "node:fs";
|
|
8396
9098
|
var STEP_TIMEOUT_MS = 600000;
|
|
@@ -8525,8 +9227,8 @@ async function sendTelemetry(event, exitCode, startedAt) {
|
|
|
8525
9227
|
}
|
|
8526
9228
|
|
|
8527
9229
|
// src/transcript.ts
|
|
8528
|
-
import { readdirSync as
|
|
8529
|
-
import { join as
|
|
9230
|
+
import { readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
9231
|
+
import { join as join7, relative } from "node:path";
|
|
8530
9232
|
var OUTPUT_LINES = 15;
|
|
8531
9233
|
var OUTPUT_CHARS = 1500;
|
|
8532
9234
|
var TEXT_CHARS = 1500;
|
|
@@ -8552,7 +9254,15 @@ var prose = (text) => prefixLines("agent: ", text);
|
|
|
8552
9254
|
function describeCall(step, name, input) {
|
|
8553
9255
|
if (typeof input.command === "string")
|
|
8554
9256
|
return prefixLines(`[${step}] $ `, input.command);
|
|
8555
|
-
for (const key of [
|
|
9257
|
+
for (const key of [
|
|
9258
|
+
"file_path",
|
|
9259
|
+
"path",
|
|
9260
|
+
"target_file",
|
|
9261
|
+
"target_directory",
|
|
9262
|
+
"url",
|
|
9263
|
+
"pattern",
|
|
9264
|
+
"query"
|
|
9265
|
+
])
|
|
8556
9266
|
if (typeof input[key] === "string")
|
|
8557
9267
|
return `[${step}] ${name} ${input[key]}`;
|
|
8558
9268
|
return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, INPUT_CHARS)}`;
|
|
@@ -8567,11 +9277,11 @@ function describeToolCall(step, c) {
|
|
|
8567
9277
|
function workdirFiles(dir) {
|
|
8568
9278
|
const out = [];
|
|
8569
9279
|
const walk = (d) => {
|
|
8570
|
-
for (const name of
|
|
9280
|
+
for (const name of readdirSync4(d)) {
|
|
8571
9281
|
if (SKIP_DIRS.has(name))
|
|
8572
9282
|
continue;
|
|
8573
|
-
const p =
|
|
8574
|
-
if (
|
|
9283
|
+
const p = join7(d, name);
|
|
9284
|
+
if (statSync2(p).isDirectory())
|
|
8575
9285
|
walk(p);
|
|
8576
9286
|
else
|
|
8577
9287
|
out.push(relative(dir, p));
|
|
@@ -8645,6 +9355,10 @@ function tier(rows) {
|
|
|
8645
9355
|
const share = graded.filter((r) => r.state === "pass").length / graded.length;
|
|
8646
9356
|
return share === 1 ? "S" : share >= 0.75 ? "A" : share >= 0.5 ? "B" : "C";
|
|
8647
9357
|
}
|
|
9358
|
+
function runTag(harness, model) {
|
|
9359
|
+
const parts = [harness, model].filter((x) => x !== null);
|
|
9360
|
+
return parts.length > 0 ? ` [${parts.join(" ")}]` : "";
|
|
9361
|
+
}
|
|
8648
9362
|
function notGradedWhy(reason) {
|
|
8649
9363
|
const said = /claude result is an error \(subtype=[^)]*\): (.*)$/s.exec(reason);
|
|
8650
9364
|
if (said)
|
|
@@ -8730,8 +9444,7 @@ function plainUI(write) {
|
|
|
8730
9444
|
printed.add(r);
|
|
8731
9445
|
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
8732
9446
|
const u = usageParts(r).join(", ");
|
|
8733
|
-
|
|
8734
|
-
write(`${label} ${r.test}#${r.n}${model} ${r.reason}${u ? ` (${u})` : ""}`);
|
|
9447
|
+
write(`${label} ${r.test}#${r.n}${runTag(r.harness, r.model)} ${r.reason}${u ? ` (${u})` : ""}`);
|
|
8735
9448
|
}
|
|
8736
9449
|
for (const r of rows) {
|
|
8737
9450
|
if (uploadPrinted.has(r) || !r.upload || r.upload === "uploading")
|
|
@@ -8806,11 +9519,14 @@ function ttyUI(out, opts) {
|
|
|
8806
9519
|
const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
8807
9520
|
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` : ""}`);
|
|
8808
9521
|
}
|
|
8809
|
-
const
|
|
9522
|
+
const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
|
|
9523
|
+
const harnessCols = column((r) => r.harness?.length ?? 0);
|
|
9524
|
+
const modelCols = column((r) => r.model?.length ?? 0);
|
|
8810
9525
|
rows.forEach((r, i) => {
|
|
8811
9526
|
out.push(p(C.gray, " │"));
|
|
8812
9527
|
const num = String(i + 1).padStart(2, "0");
|
|
8813
9528
|
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
9529
|
+
const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
|
|
8814
9530
|
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
8815
9531
|
let detail = "";
|
|
8816
9532
|
if (r.state === "running")
|
|
@@ -8819,7 +9535,7 @@ function ttyUI(out, opts) {
|
|
|
8819
9535
|
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
|
|
8820
9536
|
else if (r.state === "error")
|
|
8821
9537
|
detail = p(C.gray, usage(r));
|
|
8822
|
-
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
9538
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
8823
9539
|
if (r.state === "running" && r.command)
|
|
8824
9540
|
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
8825
9541
|
if (r.state === "fail")
|
|
@@ -8899,7 +9615,7 @@ function ttyUI(out, opts) {
|
|
|
8899
9615
|
}
|
|
8900
9616
|
|
|
8901
9617
|
// src/upload.ts
|
|
8902
|
-
import { existsSync as
|
|
9618
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
8903
9619
|
import { basename as basename2 } from "node:path";
|
|
8904
9620
|
var MAX_FILE = 20 * 1024 * 1024;
|
|
8905
9621
|
var MAX_CHECKS = 200;
|
|
@@ -8927,7 +9643,7 @@ function projectName(v) {
|
|
|
8927
9643
|
function runModel(r) {
|
|
8928
9644
|
return {
|
|
8929
9645
|
requested: label(r.model, "Model"),
|
|
8930
|
-
reported: label(r.
|
|
9646
|
+
reported: label(r.reportedModel, "Reported model")
|
|
8931
9647
|
};
|
|
8932
9648
|
}
|
|
8933
9649
|
function runGit(g) {
|
|
@@ -8981,7 +9697,7 @@ async function uploadRun(t, r) {
|
|
|
8981
9697
|
};
|
|
8982
9698
|
const bytes = {};
|
|
8983
9699
|
for (const name of FILES) {
|
|
8984
|
-
const b =
|
|
9700
|
+
const b = existsSync5(paths[name]) ? readFileSync5(paths[name]) : Buffer.alloc(0);
|
|
8985
9701
|
if (b.byteLength > MAX_FILE)
|
|
8986
9702
|
return {
|
|
8987
9703
|
status: "failed",
|
|
@@ -9063,13 +9779,13 @@ async function uploadRun(t, r) {
|
|
|
9063
9779
|
}
|
|
9064
9780
|
|
|
9065
9781
|
// src/which.ts
|
|
9066
|
-
import { accessSync, constants, statSync as
|
|
9067
|
-
import { delimiter, join as
|
|
9782
|
+
import { accessSync, constants, statSync as statSync3 } from "node:fs";
|
|
9783
|
+
import { delimiter, join as join8, resolve } from "node:path";
|
|
9068
9784
|
function resolveBin(bin) {
|
|
9069
9785
|
const executable = (p) => {
|
|
9070
9786
|
try {
|
|
9071
9787
|
accessSync(p, constants.X_OK);
|
|
9072
|
-
return
|
|
9788
|
+
return statSync3(p).isFile();
|
|
9073
9789
|
} catch {
|
|
9074
9790
|
return false;
|
|
9075
9791
|
}
|
|
@@ -9077,8 +9793,8 @@ function resolveBin(bin) {
|
|
|
9077
9793
|
if (bin.includes("/"))
|
|
9078
9794
|
return executable(bin) ? resolve(bin) : undefined;
|
|
9079
9795
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
9080
|
-
if (dir && executable(
|
|
9081
|
-
return
|
|
9796
|
+
if (dir && executable(join8(dir, bin)))
|
|
9797
|
+
return join8(dir, bin);
|
|
9082
9798
|
}
|
|
9083
9799
|
return;
|
|
9084
9800
|
}
|
|
@@ -9092,12 +9808,12 @@ function parseArgs(args) {
|
|
|
9092
9808
|
const opts = {
|
|
9093
9809
|
names: [],
|
|
9094
9810
|
models: [],
|
|
9811
|
+
harnesses: [],
|
|
9095
9812
|
parallel: 4,
|
|
9096
9813
|
json: false,
|
|
9097
9814
|
noUpload: false,
|
|
9098
9815
|
dryRun: false,
|
|
9099
|
-
vars: {}
|
|
9100
|
-
claudeBin: "claude"
|
|
9816
|
+
vars: {}
|
|
9101
9817
|
};
|
|
9102
9818
|
for (let i = 0;i < args.length; i++) {
|
|
9103
9819
|
const a = args[i];
|
|
@@ -9138,14 +9854,21 @@ function parseArgs(args) {
|
|
|
9138
9854
|
if (opts.models.includes(v))
|
|
9139
9855
|
throw new UsageError(`--model ${v} given twice`);
|
|
9140
9856
|
opts.models.push(v);
|
|
9857
|
+
} else if (flag === "--harness") {
|
|
9858
|
+
const v = value();
|
|
9859
|
+
if (!HARNESSES.has(v))
|
|
9860
|
+
throw new UsageError(`Unknown harness ${JSON.stringify(v)} ${validHarnesses()}`);
|
|
9861
|
+
if (opts.harnesses.includes(v))
|
|
9862
|
+
throw new UsageError(`--harness ${v} given twice`);
|
|
9863
|
+
opts.harnesses.push(v);
|
|
9141
9864
|
} else if (flag === "--parallel") {
|
|
9142
9865
|
const v = value();
|
|
9143
9866
|
const n = Number(v);
|
|
9144
9867
|
if (!/^\d+$/.test(v) || n < 1)
|
|
9145
9868
|
throw new UsageError(`--parallel must be a positive integer, got ${v}`);
|
|
9146
9869
|
opts.parallel = n;
|
|
9147
|
-
} else if (flag === "--
|
|
9148
|
-
opts.
|
|
9870
|
+
} else if (flag === "--harness-bin")
|
|
9871
|
+
opts.harnessBin = value();
|
|
9149
9872
|
else if (a.startsWith("-"))
|
|
9150
9873
|
throw new UsageError(`Unknown flag: ${a}`);
|
|
9151
9874
|
else
|
|
@@ -9158,7 +9881,7 @@ function newRunId() {
|
|
|
9158
9881
|
return `${ts}-${randomBytes2(2).toString("hex")}`;
|
|
9159
9882
|
}
|
|
9160
9883
|
function writeRunJson(r) {
|
|
9161
|
-
writeFileSync4(
|
|
9884
|
+
writeFileSync4(join9(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
9162
9885
|
`);
|
|
9163
9886
|
}
|
|
9164
9887
|
async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
@@ -9205,59 +9928,76 @@ function runFill(runId, n, testName, out, dir) {
|
|
|
9205
9928
|
"run.n": String(n),
|
|
9206
9929
|
"test.name": testName,
|
|
9207
9930
|
FIREGOAT_OUT: out,
|
|
9208
|
-
FIREGOAT_WORKDIR:
|
|
9209
|
-
FIREGOAT_HOME:
|
|
9931
|
+
FIREGOAT_WORKDIR: join9(dir, "work"),
|
|
9932
|
+
FIREGOAT_HOME: join9(dir, "home")
|
|
9933
|
+
};
|
|
9934
|
+
}
|
|
9935
|
+
function renderHarnessArgs(test, vars, fill) {
|
|
9936
|
+
return test.harnessArgs.map((a) => renderValue(a.arg, vars, fill, a.path));
|
|
9937
|
+
}
|
|
9938
|
+
function renderSteps(what, commands, vars, fill) {
|
|
9939
|
+
return commands.map((c, i) => renderStep(c, vars, fill, `${what}[${i}]`));
|
|
9940
|
+
}
|
|
9941
|
+
function renderRunSteps(test, top, vars, fill) {
|
|
9942
|
+
return {
|
|
9943
|
+
install: renderSteps("install", [...top.install, ...test.install], vars, fill),
|
|
9944
|
+
checks: renderSteps("checks", test.checks.map((c) => c.run), vars, fill),
|
|
9945
|
+
cleanup: renderSteps("cleanup", [...test.cleanup, ...top.cleanup], vars, fill)
|
|
9210
9946
|
};
|
|
9211
9947
|
}
|
|
9212
|
-
function
|
|
9213
|
-
return
|
|
9948
|
+
function renderBuild(config, vars, runId) {
|
|
9949
|
+
return renderSteps("build", config.build, vars, { "run.id": runId });
|
|
9214
9950
|
}
|
|
9215
|
-
function planRuns(tests, models, runs) {
|
|
9216
|
-
const
|
|
9217
|
-
|
|
9951
|
+
function planRuns(tests, harnesses, models, runs) {
|
|
9952
|
+
const modelList = models.length > 0 ? models : [null];
|
|
9953
|
+
const matrix = harnesses.flatMap((harness) => modelList.map((model) => ({ harness, model })));
|
|
9954
|
+
return tests.flatMap((t) => matrix.flatMap((cell, c) => Array.from({ length: runs }, (_, i) => ({
|
|
9218
9955
|
test: t.name,
|
|
9219
|
-
n:
|
|
9220
|
-
|
|
9956
|
+
n: c * runs + i + 1,
|
|
9957
|
+
...cell
|
|
9221
9958
|
}))));
|
|
9222
9959
|
}
|
|
9960
|
+
function showsHarness(harnesses) {
|
|
9961
|
+
return harnesses.length > 1 || harnesses[0] !== DEFAULT_HARNESS;
|
|
9962
|
+
}
|
|
9223
9963
|
var DRY_RUN_ID = "<runId>";
|
|
9224
9964
|
function dryRun(opts) {
|
|
9225
|
-
const out =
|
|
9965
|
+
const out = join9(opts.runsRoot, DRY_RUN_ID, "out");
|
|
9226
9966
|
const runs = opts.plan.map((p) => {
|
|
9227
9967
|
const test = opts.tests.find((t) => t.name === p.test);
|
|
9228
|
-
const fill = runFill(DRY_RUN_ID, p.n, test.name, out,
|
|
9968
|
+
const fill = runFill(DRY_RUN_ID, p.n, test.name, out, join9(opts.runsRoot, DRY_RUN_ID, test.name, String(p.n)));
|
|
9229
9969
|
renderEnv(test.env, opts.vars, fill);
|
|
9230
9970
|
return {
|
|
9231
9971
|
...p,
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
checks: test.checks.map((c) => render(c.run, opts.vars)),
|
|
9235
|
-
cleanup: [...test.cleanup, ...opts.config.cleanup]
|
|
9972
|
+
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
9973
|
+
...renderRunSteps(test, opts.config, opts.vars, fill)
|
|
9236
9974
|
};
|
|
9237
9975
|
});
|
|
9976
|
+
const build = renderBuild(opts.config, opts.vars, DRY_RUN_ID);
|
|
9238
9977
|
if (opts.json) {
|
|
9239
|
-
console.log(JSON.stringify({ dryRun: true, build
|
|
9978
|
+
console.log(JSON.stringify({ dryRun: true, build, runs }, null, 2));
|
|
9240
9979
|
return;
|
|
9241
9980
|
}
|
|
9242
9981
|
const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
9243
9982
|
opts.say(`Dry run: ${count(runs.length, "run")}, build ${count(opts.config.build.length, "step")}. Nothing was started.`);
|
|
9244
9983
|
for (const r of runs)
|
|
9245
|
-
opts.say(`${r.test}#${r.n}${
|
|
9984
|
+
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)}` : ""}`);
|
|
9246
9985
|
}
|
|
9247
9986
|
async function runOne(opts) {
|
|
9248
9987
|
const { test, n } = opts;
|
|
9249
9988
|
const started = Date.now();
|
|
9250
|
-
const dir =
|
|
9251
|
-
const workdir =
|
|
9989
|
+
const dir = join9(opts.runDir, test.name, String(n));
|
|
9990
|
+
const workdir = join9(dir, "work");
|
|
9252
9991
|
mkdirSync4(workdir, { recursive: true });
|
|
9253
9992
|
const { home, envFile } = makeRunHome(dir);
|
|
9254
9993
|
const fill = runFill(opts.runId, n, test.name, opts.out, dir);
|
|
9255
9994
|
const r = {
|
|
9256
9995
|
test: test.name,
|
|
9257
9996
|
n,
|
|
9997
|
+
harness: opts.harness.name,
|
|
9258
9998
|
model: opts.model,
|
|
9259
|
-
|
|
9260
|
-
|
|
9999
|
+
reportedModel: null,
|
|
10000
|
+
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
9261
10001
|
status: "error",
|
|
9262
10002
|
reason: "",
|
|
9263
10003
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -9267,11 +10007,11 @@ async function runOne(opts) {
|
|
|
9267
10007
|
workdir,
|
|
9268
10008
|
home,
|
|
9269
10009
|
session: null,
|
|
9270
|
-
transcript:
|
|
9271
|
-
stderr:
|
|
9272
|
-
checksLog:
|
|
10010
|
+
transcript: join9(dir, "transcript.txt"),
|
|
10011
|
+
stderr: join9(dir, "stderr.txt"),
|
|
10012
|
+
checksLog: join9(dir, "checks.txt"),
|
|
9273
10013
|
cleanup: [],
|
|
9274
|
-
cleanupLog:
|
|
10014
|
+
cleanupLog: join9(dir, "cleanup.txt"),
|
|
9275
10015
|
num_turns: null,
|
|
9276
10016
|
cost: null,
|
|
9277
10017
|
duration_ms: 0,
|
|
@@ -9286,11 +10026,11 @@ async function runOne(opts) {
|
|
|
9286
10026
|
FIREGOAT_WORKDIR: workdir,
|
|
9287
10027
|
FIREGOAT_HOME: home
|
|
9288
10028
|
};
|
|
9289
|
-
const
|
|
9290
|
-
const
|
|
9291
|
-
if (cleanup.length > 0)
|
|
10029
|
+
const steps = renderRunSteps(test, opts, opts.vars, fill);
|
|
10030
|
+
const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile);
|
|
10031
|
+
if (steps.cleanup.length > 0)
|
|
9292
10032
|
r.cleanup = await runAllSteps({
|
|
9293
|
-
commands: cleanup,
|
|
10033
|
+
commands: steps.cleanup,
|
|
9294
10034
|
cwd: workdir,
|
|
9295
10035
|
env: stepEnv,
|
|
9296
10036
|
logPath: r.cleanupLog
|
|
@@ -9301,17 +10041,16 @@ async function runOne(opts) {
|
|
|
9301
10041
|
writeRunJson(r);
|
|
9302
10042
|
return r;
|
|
9303
10043
|
}
|
|
9304
|
-
async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
10044
|
+
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
9305
10045
|
const { test, n } = opts;
|
|
9306
10046
|
const { dir, workdir } = r;
|
|
9307
|
-
|
|
9308
|
-
if (install.length > 0) {
|
|
10047
|
+
if (steps.install.length > 0) {
|
|
9309
10048
|
const error = await runSteps({
|
|
9310
10049
|
what: "install",
|
|
9311
|
-
commands: install,
|
|
10050
|
+
commands: steps.install,
|
|
9312
10051
|
cwd: workdir,
|
|
9313
10052
|
env: stepEnv,
|
|
9314
|
-
logPath:
|
|
10053
|
+
logPath: join9(dir, "install.txt")
|
|
9315
10054
|
});
|
|
9316
10055
|
if (error)
|
|
9317
10056
|
return ["error", `not graded: ${error}`];
|
|
@@ -9325,7 +10064,10 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9325
10064
|
env: {
|
|
9326
10065
|
...hostEnv(),
|
|
9327
10066
|
...env,
|
|
9328
|
-
...harness.env({
|
|
10067
|
+
...harness.env({
|
|
10068
|
+
configDir: opts.configDir,
|
|
10069
|
+
run: { home: r.home, homeEnvFile }
|
|
10070
|
+
}),
|
|
9329
10071
|
PATH: opts.stepEnv.PATH,
|
|
9330
10072
|
FIREGOAT_OUT: opts.out,
|
|
9331
10073
|
FIREGOAT_WORKDIR: workdir,
|
|
@@ -9334,18 +10076,22 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9334
10076
|
stderr,
|
|
9335
10077
|
timeoutMs: test.timeout * 1000,
|
|
9336
10078
|
model: opts.model ?? undefined,
|
|
9337
|
-
extraArgs: r.
|
|
10079
|
+
extraArgs: r.harnessArgs,
|
|
9338
10080
|
onProgress: opts.onProgress
|
|
9339
10081
|
});
|
|
9340
|
-
r.claudeModel = outcome.reportedModel;
|
|
9341
10082
|
if (outcome.version !== null)
|
|
9342
10083
|
evalStats({ claudeVersion: claudeVersion(outcome.version) });
|
|
9343
10084
|
await new Promise((res) => stderr.end(res));
|
|
9344
10085
|
if (outcome.sessionId)
|
|
9345
10086
|
r.session = harness.findSession(opts.configDir, outcome.sessionId);
|
|
10087
|
+
r.reportedModel = outcome.reportedModel ?? (r.session && harness.sessionModel ? harness.sessionModel(r.session) : null);
|
|
10088
|
+
const about = [
|
|
10089
|
+
opts.shownHarness,
|
|
10090
|
+
opts.model === null ? null : `model ${opts.model}`
|
|
10091
|
+
].filter((x) => x !== null);
|
|
9346
10092
|
writeTranscript({
|
|
9347
10093
|
path: r.transcript,
|
|
9348
|
-
title: `${test.name}#${n}${
|
|
10094
|
+
title: `${test.name}#${n}${about.length > 0 ? ` (${about.join(", ")})` : ""}`,
|
|
9349
10095
|
prompt: r.prompt,
|
|
9350
10096
|
steps: outcome.steps,
|
|
9351
10097
|
final: outcome.final,
|
|
@@ -9362,9 +10108,9 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9362
10108
|
];
|
|
9363
10109
|
const log = createWriteStream2(r.checksLog);
|
|
9364
10110
|
try {
|
|
9365
|
-
for (const
|
|
10111
|
+
for (const run of steps.checks)
|
|
9366
10112
|
r.checks.push(await runShell({
|
|
9367
|
-
run
|
|
10113
|
+
run,
|
|
9368
10114
|
cwd: workdir,
|
|
9369
10115
|
env: {
|
|
9370
10116
|
...stepEnv,
|
|
@@ -9388,6 +10134,9 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9388
10134
|
test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
|
|
9389
10135
|
];
|
|
9390
10136
|
}
|
|
10137
|
+
function notUploaded(h) {
|
|
10138
|
+
return `firegoat.ai doesn't read ${h.name} sessions yet: its runs stay local`;
|
|
10139
|
+
}
|
|
9391
10140
|
var MAX_LISTED_URLS = 5;
|
|
9392
10141
|
function uploadedSummary(origin, results, runDir) {
|
|
9393
10142
|
const urls = results.flatMap((r) => r.upload?.status === "uploaded" ? [r.upload.url] : []);
|
|
@@ -9395,7 +10144,7 @@ function uploadedSummary(origin, results, runDir) {
|
|
|
9395
10144
|
return;
|
|
9396
10145
|
const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
|
|
9397
10146
|
if (urls.length > MAX_LISTED_URLS) {
|
|
9398
|
-
console.log(`${head}: each run's upload.url in ${
|
|
10147
|
+
console.log(`${head}: each run's upload.url in ${join9(runDir, "summary.json")}`);
|
|
9399
10148
|
return;
|
|
9400
10149
|
}
|
|
9401
10150
|
console.log(`${head}:`);
|
|
@@ -9450,9 +10199,22 @@ async function evalCommand(args, root) {
|
|
|
9450
10199
|
}
|
|
9451
10200
|
const runs = opts.runs ?? config.runs;
|
|
9452
10201
|
const models = opts.models.length > 0 ? opts.models : config.models;
|
|
10202
|
+
const harnessNames = opts.harnesses.length > 0 ? opts.harnesses : config.harnesses;
|
|
10203
|
+
if (opts.harnessBin !== undefined && harnessNames.length !== 1)
|
|
10204
|
+
return fatal(EXIT.usage, `--harness-bin needs exactly one harness, this eval runs ${harnessNames.length} (${harnessNames.join(", ")}): pick one with --harness`, "usage");
|
|
10205
|
+
const harnesses = harnessNames.map((h) => HARNESSES.get(h));
|
|
10206
|
+
const showHarness = showsHarness(harnessNames);
|
|
10207
|
+
try {
|
|
10208
|
+
for (const h of harnesses)
|
|
10209
|
+
checkHarnessArgs(tests, h);
|
|
10210
|
+
} catch (e) {
|
|
10211
|
+
if (e instanceof ConfigError)
|
|
10212
|
+
return fatal(EXIT.usage, e.message, "config");
|
|
10213
|
+
throw e;
|
|
10214
|
+
}
|
|
9453
10215
|
evalStats({
|
|
9454
10216
|
testCount: tests.length,
|
|
9455
|
-
runCount: tests.length * runs * Math.max(models.length, 1),
|
|
10217
|
+
runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
|
|
9456
10218
|
modelCount: models.length,
|
|
9457
10219
|
parallel: opts.parallel
|
|
9458
10220
|
});
|
|
@@ -9462,8 +10224,10 @@ async function evalCommand(args, root) {
|
|
|
9462
10224
|
t.prompt,
|
|
9463
10225
|
...t.checks.map((c) => c.run),
|
|
9464
10226
|
...Object.values(t.env),
|
|
9465
|
-
...t.
|
|
9466
|
-
|
|
10227
|
+
...t.harnessArgs.map((a) => a.arg),
|
|
10228
|
+
...t.install,
|
|
10229
|
+
...t.cleanup
|
|
10230
|
+
]).concat(config.build, config.install, config.cleanup).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
9467
10231
|
];
|
|
9468
10232
|
if (missing.length > 0)
|
|
9469
10233
|
return fatal(EXIT.usage, `No value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`, "config");
|
|
@@ -9478,9 +10242,10 @@ async function evalCommand(args, root) {
|
|
|
9478
10242
|
dryRun({
|
|
9479
10243
|
config,
|
|
9480
10244
|
tests,
|
|
9481
|
-
plan: planRuns(tests, models, runs),
|
|
10245
|
+
plan: planRuns(tests, harnessNames, models, runs),
|
|
10246
|
+
showHarness,
|
|
9482
10247
|
vars,
|
|
9483
|
-
runsRoot:
|
|
10248
|
+
runsRoot: join9(repo?.mainCheckout ?? root, ".firegoat", "runs"),
|
|
9484
10249
|
json: opts.json,
|
|
9485
10250
|
say
|
|
9486
10251
|
});
|
|
@@ -9491,11 +10256,15 @@ async function evalCommand(args, root) {
|
|
|
9491
10256
|
}
|
|
9492
10257
|
return EXIT.pass;
|
|
9493
10258
|
}
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
const
|
|
9497
|
-
|
|
9498
|
-
|
|
10259
|
+
evalStats({ harness: harnessNames.join(",") });
|
|
10260
|
+
const bins = new Map;
|
|
10261
|
+
for (const h of harnesses) {
|
|
10262
|
+
const bin = opts.harnessBin ?? h.bin;
|
|
10263
|
+
const path = resolveBin(bin);
|
|
10264
|
+
if (!path)
|
|
10265
|
+
return fatal(EXIT.notGraded, `Not graded: ${h.name} binary ${JSON.stringify(bin)} not found. ${h.installHint}`, "harness_missing");
|
|
10266
|
+
bins.set(h, path);
|
|
10267
|
+
}
|
|
9499
10268
|
let repo;
|
|
9500
10269
|
try {
|
|
9501
10270
|
repo = gitRepo(root);
|
|
@@ -9503,16 +10272,24 @@ async function evalCommand(args, root) {
|
|
|
9503
10272
|
return fatal(EXIT.usage, e.message, "config");
|
|
9504
10273
|
}
|
|
9505
10274
|
const firegoatDir = ensureFiregoatDir(repo?.mainCheckout ?? root);
|
|
9506
|
-
const
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
10275
|
+
const configDirs = new Map;
|
|
10276
|
+
for (const h of harnesses) {
|
|
10277
|
+
const configDir = join9(firegoatDir, h.configDirName);
|
|
10278
|
+
mkdirSync4(configDir, { recursive: true });
|
|
10279
|
+
configDirs.set(h, configDir);
|
|
10280
|
+
}
|
|
10281
|
+
ui.auth(harnesses.length === 1 ? harnesses[0].authLabel() : harnesses.map((h) => `${h.name}: ${h.authLabel()}`).join("; "));
|
|
10282
|
+
for (const h of harnesses) {
|
|
10283
|
+
const configDir = configDirs.get(h);
|
|
10284
|
+
const loginProblem = h.checkLogin({
|
|
10285
|
+
bin: bins.get(h),
|
|
10286
|
+
env: { ...hostEnv(), ...h.env({ configDir }) },
|
|
10287
|
+
cwd: root,
|
|
10288
|
+
configDir
|
|
10289
|
+
});
|
|
10290
|
+
if (loginProblem)
|
|
10291
|
+
return fatal(EXIT.notGraded, `Not graded: ${loginProblem.reason}`, loginProblem.loggedOut ? "harness_logged_out" : "harness_auth_status");
|
|
10292
|
+
}
|
|
9516
10293
|
const evalStarted = Date.now();
|
|
9517
10294
|
const runId = newRunId();
|
|
9518
10295
|
let upload;
|
|
@@ -9521,14 +10298,19 @@ async function evalCommand(args, root) {
|
|
|
9521
10298
|
if (pre && "exitCode" in pre)
|
|
9522
10299
|
return fatal(pre.exitCode, pre.error, pre.code);
|
|
9523
10300
|
upload = pre;
|
|
10301
|
+
if (upload) {
|
|
10302
|
+
for (const h of harnesses)
|
|
10303
|
+
if (!h.uploads)
|
|
10304
|
+
ui.note(notUploaded(h));
|
|
10305
|
+
}
|
|
9524
10306
|
} catch (e) {
|
|
9525
10307
|
if (e instanceof ConfigError)
|
|
9526
10308
|
return fatal(EXIT.usage, e.message, "config");
|
|
9527
10309
|
throw e;
|
|
9528
10310
|
}
|
|
9529
|
-
const runsRoot =
|
|
9530
|
-
const runDir =
|
|
9531
|
-
const out =
|
|
10311
|
+
const runsRoot = join9(firegoatDir, "runs");
|
|
10312
|
+
const runDir = join9(runsRoot, runId);
|
|
10313
|
+
const out = join9(runDir, "out");
|
|
9532
10314
|
mkdirSync4(out, { recursive: true });
|
|
9533
10315
|
const stepEnv = {
|
|
9534
10316
|
...hostEnv(),
|
|
@@ -9536,8 +10318,10 @@ async function evalCommand(args, root) {
|
|
|
9536
10318
|
FIREGOAT_OUT: out,
|
|
9537
10319
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
9538
10320
|
};
|
|
9539
|
-
const
|
|
10321
|
+
const plan = planRuns(tests, harnessNames, models, runs);
|
|
10322
|
+
const rows = plan.map((p) => ({
|
|
9540
10323
|
...p,
|
|
10324
|
+
harness: showHarness ? p.harness : null,
|
|
9541
10325
|
state: "queued",
|
|
9542
10326
|
turns: 0
|
|
9543
10327
|
}));
|
|
@@ -9546,13 +10330,13 @@ async function evalCommand(args, root) {
|
|
|
9546
10330
|
ui.build("running");
|
|
9547
10331
|
const error = await runSteps({
|
|
9548
10332
|
what: "build",
|
|
9549
|
-
commands: config
|
|
10333
|
+
commands: renderBuild(config, vars, runId),
|
|
9550
10334
|
cwd: root,
|
|
9551
10335
|
env: {
|
|
9552
10336
|
...stepEnv,
|
|
9553
10337
|
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
|
|
9554
10338
|
},
|
|
9555
|
-
logPath:
|
|
10339
|
+
logPath: join9(runDir, "build.txt")
|
|
9556
10340
|
});
|
|
9557
10341
|
const secs = Math.round((Date.now() - buildStarted) / 1000);
|
|
9558
10342
|
ui.build(error ? "fail" : "ok", secs);
|
|
@@ -9564,6 +10348,7 @@ async function evalCommand(args, root) {
|
|
|
9564
10348
|
const runAt = async (i) => {
|
|
9565
10349
|
const row = rows[i];
|
|
9566
10350
|
const test = tests.find((t) => t.name === row.test);
|
|
10351
|
+
const harness = HARNESSES.get(plan[i].harness);
|
|
9567
10352
|
row.state = "running";
|
|
9568
10353
|
ui.update();
|
|
9569
10354
|
const r = await runOne({
|
|
@@ -9575,11 +10360,12 @@ async function evalCommand(args, root) {
|
|
|
9575
10360
|
test,
|
|
9576
10361
|
n: row.n,
|
|
9577
10362
|
model: row.model,
|
|
10363
|
+
shownHarness: row.harness,
|
|
9578
10364
|
runId,
|
|
9579
10365
|
runDir,
|
|
9580
10366
|
harness,
|
|
9581
|
-
bin:
|
|
9582
|
-
configDir,
|
|
10367
|
+
bin: bins.get(harness),
|
|
10368
|
+
configDir: configDirs.get(harness),
|
|
9583
10369
|
install: config.install,
|
|
9584
10370
|
cleanup: config.cleanup,
|
|
9585
10371
|
out,
|
|
@@ -9597,7 +10383,10 @@ async function evalCommand(args, root) {
|
|
|
9597
10383
|
command: undefined
|
|
9598
10384
|
});
|
|
9599
10385
|
ui.update();
|
|
9600
|
-
if (upload) {
|
|
10386
|
+
if (upload && !harness.uploads) {
|
|
10387
|
+
r.upload = { status: "skipped", reason: notUploaded(harness) };
|
|
10388
|
+
writeRunJson(r);
|
|
10389
|
+
} else if (upload) {
|
|
9601
10390
|
row.upload = "uploading";
|
|
9602
10391
|
ui.update();
|
|
9603
10392
|
r.upload = await uploadRun(upload, r);
|
|
@@ -9624,24 +10413,34 @@ async function evalCommand(args, root) {
|
|
|
9624
10413
|
uploaded,
|
|
9625
10414
|
uploadFailed: uploadsFailed
|
|
9626
10415
|
});
|
|
10416
|
+
const status = totals.error > 0 ? "error" : totals.fail > 0 ? "fail" : "pass";
|
|
10417
|
+
const exitCode = {
|
|
10418
|
+
error: EXIT.notGraded,
|
|
10419
|
+
fail: EXIT.fail,
|
|
10420
|
+
pass: EXIT.pass
|
|
10421
|
+
}[status];
|
|
9627
10422
|
const doc = {
|
|
9628
10423
|
firegoatVersion: package_default.version,
|
|
9629
10424
|
runId,
|
|
9630
10425
|
runsDir: runDir,
|
|
9631
10426
|
dir: root,
|
|
9632
10427
|
...repo && { git: repo.info },
|
|
10428
|
+
harnesses: harnessNames,
|
|
9633
10429
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
10430
|
+
runs: results,
|
|
9634
10431
|
totals,
|
|
9635
|
-
|
|
10432
|
+
status,
|
|
10433
|
+
exitCode
|
|
9636
10434
|
};
|
|
9637
|
-
writeFileSync4(
|
|
10435
|
+
writeFileSync4(join9(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
9638
10436
|
`);
|
|
9639
|
-
appendFileSync(
|
|
10437
|
+
appendFileSync(join9(runsRoot, "index.jsonl"), `${JSON.stringify({
|
|
9640
10438
|
runId,
|
|
9641
10439
|
startedAt: new Date(evalStarted).toISOString(),
|
|
9642
10440
|
dir: root,
|
|
9643
10441
|
branch: repo?.info.branch ?? null,
|
|
9644
10442
|
commit: repo?.info.commit ?? null,
|
|
10443
|
+
harnesses: harnessNames,
|
|
9645
10444
|
passed: totals.pass,
|
|
9646
10445
|
failed: totals.fail,
|
|
9647
10446
|
notGraded: totals.error,
|
|
@@ -9666,15 +10465,15 @@ async function evalCommand(args, root) {
|
|
|
9666
10465
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
9667
10466
|
if (opts.json)
|
|
9668
10467
|
console.log(JSON.stringify(doc, null, 2));
|
|
9669
|
-
if (
|
|
10468
|
+
if (status === "error")
|
|
9670
10469
|
failWith("not_graded");
|
|
9671
|
-
else if (
|
|
10470
|
+
else if (status === "fail")
|
|
9672
10471
|
failWith("checks_failed");
|
|
9673
|
-
return
|
|
10472
|
+
return exitCode;
|
|
9674
10473
|
}
|
|
9675
10474
|
|
|
9676
10475
|
// src/feedback.ts
|
|
9677
|
-
import { readFileSync as
|
|
10476
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
9678
10477
|
|
|
9679
10478
|
// node_modules/@firegoat/feedback/dist/index.js
|
|
9680
10479
|
function memoryDeviceStore() {
|
|
@@ -9893,7 +10692,7 @@ async function feedbackCommand(args) {
|
|
|
9893
10692
|
try {
|
|
9894
10693
|
if (a === "--body") {
|
|
9895
10694
|
const v = value();
|
|
9896
|
-
body = v === "-" ?
|
|
10695
|
+
body = v === "-" ? readFileSync6(0, "utf8") : v;
|
|
9897
10696
|
} else if (a === "--type") {
|
|
9898
10697
|
const v = value();
|
|
9899
10698
|
if (!TYPES.includes(v))
|
|
@@ -9946,10 +10745,9 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
9946
10745
|
}
|
|
9947
10746
|
|
|
9948
10747
|
// src/init.ts
|
|
9949
|
-
import { spawnSync as
|
|
9950
|
-
import { existsSync as
|
|
9951
|
-
import { basename as basename4, join as
|
|
9952
|
-
import { createInterface } from "node:readline/promises";
|
|
10748
|
+
import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
|
|
10749
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10750
|
+
import { basename as basename4, join as join10 } from "node:path";
|
|
9953
10751
|
|
|
9954
10752
|
// src/login.ts
|
|
9955
10753
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -9960,7 +10758,7 @@ var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
|
9960
10758
|
var UPLOADS_ON = "Eval uploads each session to firegoat.ai as a private trace; --no-upload keeps runs local.";
|
|
9961
10759
|
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).`;
|
|
9962
10760
|
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).";
|
|
9963
|
-
function
|
|
10761
|
+
function str3(o, k, step) {
|
|
9964
10762
|
const v = o?.[k];
|
|
9965
10763
|
if (typeof v !== "string" || v === "")
|
|
9966
10764
|
throw new ApiError(`${step}: response has no ${k}`);
|
|
@@ -9979,7 +10777,7 @@ function openBrowser(url) {
|
|
|
9979
10777
|
} catch {}
|
|
9980
10778
|
}
|
|
9981
10779
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9982
|
-
async function loginFlow(
|
|
10780
|
+
async function loginFlow(show, interactive) {
|
|
9983
10781
|
const base = baseUrl();
|
|
9984
10782
|
const origin = new URL(base).origin;
|
|
9985
10783
|
const cfgStep = "GET /api/traces/oauth/config";
|
|
@@ -9991,9 +10789,9 @@ async function loginFlow(say, interactive) {
|
|
|
9991
10789
|
});
|
|
9992
10790
|
if (cfg.status !== 200)
|
|
9993
10791
|
throw replyError(cfgStep, cfg);
|
|
9994
|
-
const clientId =
|
|
9995
|
-
const deviceUrl = assertSafeUrl(
|
|
9996
|
-
const tokenUrl = assertSafeUrl(
|
|
10792
|
+
const clientId = str3(cfg.json, "client_id", cfgStep);
|
|
10793
|
+
const deviceUrl = assertSafeUrl(str3(cfg.json, "device_authorization_endpoint", cfgStep), "device_authorization_endpoint");
|
|
10794
|
+
const tokenUrl = assertSafeUrl(str3(cfg.json, "token_endpoint", cfgStep), "token_endpoint");
|
|
9997
10795
|
if (tokenUrl.origin !== origin)
|
|
9998
10796
|
throw new ApiError(`token_endpoint must be on ${origin}, got ${printable(tokenUrl.origin, 100)}`);
|
|
9999
10797
|
const devStep = "Device authorization";
|
|
@@ -10007,11 +10805,11 @@ async function loginFlow(say, interactive) {
|
|
|
10007
10805
|
});
|
|
10008
10806
|
if (dev.status !== 200)
|
|
10009
10807
|
throw replyError(devStep, dev);
|
|
10010
|
-
const deviceCode =
|
|
10011
|
-
const userCode =
|
|
10808
|
+
const deviceCode = str3(dev.json, "device_code", devStep);
|
|
10809
|
+
const userCode = str3(dev.json, "user_code", devStep);
|
|
10012
10810
|
if (!/^[A-Za-z0-9-]{1,64}$/.test(userCode))
|
|
10013
10811
|
throw new ApiError(`${devStep}: user_code has unexpected characters`);
|
|
10014
|
-
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete :
|
|
10812
|
+
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete : str3(dev.json, "verification_uri", devStep);
|
|
10015
10813
|
const page = assertSafeUrl(pageRaw, "verification_uri");
|
|
10016
10814
|
if (page.protocol !== "https:" || pageRaw.startsWith("-"))
|
|
10017
10815
|
throw new ApiError("verification_uri must be https");
|
|
@@ -10024,8 +10822,11 @@ async function loginFlow(say, interactive) {
|
|
|
10024
10822
|
let interval = Math.min(Math.max(rawInterval ?? 5, 5), MAX_INTERVAL_S);
|
|
10025
10823
|
const expiresS = Math.min(expiresIn, MAX_EXPIRES_S);
|
|
10026
10824
|
const deadline = Date.now() + expiresS * 1000;
|
|
10027
|
-
|
|
10028
|
-
|
|
10825
|
+
show({
|
|
10826
|
+
page: printable(page.href, 500),
|
|
10827
|
+
code: userCode,
|
|
10828
|
+
minutes: Math.max(1, Math.round(expiresS / 60))
|
|
10829
|
+
});
|
|
10029
10830
|
if (interactive)
|
|
10030
10831
|
openBrowser(page.href);
|
|
10031
10832
|
const tokStep = "Token request";
|
|
@@ -10093,7 +10894,10 @@ ${LOGIN_HELP}`);
|
|
|
10093
10894
|
console.error("Note: FIREGOAT_API_KEY is set, so eval uses it instead of this login.");
|
|
10094
10895
|
try {
|
|
10095
10896
|
console.log(UPLOAD_NOTICE);
|
|
10096
|
-
const email = await loginFlow((
|
|
10897
|
+
const email = await loginFlow(({ page, code, minutes }) => {
|
|
10898
|
+
console.log(TERMS);
|
|
10899
|
+
console.log(`Open ${page} and enter code ${code} (expires in ${minutes}m).`);
|
|
10900
|
+
}, Boolean(process.stdout.isTTY));
|
|
10097
10901
|
console.log(`Logged in as ${email}. Key saved to ${credentialsPath()}.`);
|
|
10098
10902
|
return EXIT.pass;
|
|
10099
10903
|
} catch (e) {
|
|
@@ -10130,6 +10934,102 @@ ${LOGOUT_HELP}`);
|
|
|
10130
10934
|
return EXIT.pass;
|
|
10131
10935
|
}
|
|
10132
10936
|
|
|
10937
|
+
// src/tty.ts
|
|
10938
|
+
import { emitKeypressEvents } from "node:readline";
|
|
10939
|
+
var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
10940
|
+
var visible = (s) => s.replace(ANSI2, "");
|
|
10941
|
+
function liveBlock(out = process.stdout) {
|
|
10942
|
+
let drawn = 0;
|
|
10943
|
+
const rowsOf = (line) => Math.max(1, Math.ceil(visible(line).length / (out.columns || 80)));
|
|
10944
|
+
return {
|
|
10945
|
+
draw(lines) {
|
|
10946
|
+
out.write(`${drawn > 0 ? `\x1B[${drawn}A\x1B[J` : ""}${lines.join(`
|
|
10947
|
+
`)}
|
|
10948
|
+
`);
|
|
10949
|
+
drawn = lines.reduce((n, l) => n + rowsOf(l), 0);
|
|
10950
|
+
}
|
|
10951
|
+
};
|
|
10952
|
+
}
|
|
10953
|
+
function animate(block, render) {
|
|
10954
|
+
let frame = 0;
|
|
10955
|
+
const draw = () => block.draw(render(SPIN[frame++ % SPIN.length]));
|
|
10956
|
+
draw();
|
|
10957
|
+
const timer = setInterval(draw, 120);
|
|
10958
|
+
return () => clearInterval(timer);
|
|
10959
|
+
}
|
|
10960
|
+
function wrap(text, indent, width) {
|
|
10961
|
+
const room = Math.max(10, width - indent.length);
|
|
10962
|
+
const lines = [];
|
|
10963
|
+
let line = "";
|
|
10964
|
+
for (const word of text.split(" ")) {
|
|
10965
|
+
for (let w = word;w.length > 0; ) {
|
|
10966
|
+
const next = line ? `${line} ${w}` : w;
|
|
10967
|
+
if (next.length <= room) {
|
|
10968
|
+
line = next;
|
|
10969
|
+
w = "";
|
|
10970
|
+
} else if (line) {
|
|
10971
|
+
lines.push(line);
|
|
10972
|
+
line = "";
|
|
10973
|
+
} else {
|
|
10974
|
+
lines.push(w.slice(0, room));
|
|
10975
|
+
w = w.slice(room);
|
|
10976
|
+
}
|
|
10977
|
+
}
|
|
10978
|
+
}
|
|
10979
|
+
if (line)
|
|
10980
|
+
lines.push(line);
|
|
10981
|
+
return lines.map((l) => indent + l);
|
|
10982
|
+
}
|
|
10983
|
+
function abortError() {
|
|
10984
|
+
return Object.assign(new Error("Aborted"), { code: "ABORT_ERR" });
|
|
10985
|
+
}
|
|
10986
|
+
function select(block, count, render, initial = 0) {
|
|
10987
|
+
const input = process.stdin;
|
|
10988
|
+
let current = initial;
|
|
10989
|
+
block.draw(render(current));
|
|
10990
|
+
return new Promise((resolve, reject) => {
|
|
10991
|
+
const restore = () => {
|
|
10992
|
+
input.off("keypress", onKey);
|
|
10993
|
+
input.off("end", onEnd);
|
|
10994
|
+
process.off("exit", showCursor);
|
|
10995
|
+
input.setRawMode(false);
|
|
10996
|
+
input.pause();
|
|
10997
|
+
showCursor();
|
|
10998
|
+
};
|
|
10999
|
+
const onKey = (_, key) => {
|
|
11000
|
+
if (!key)
|
|
11001
|
+
return;
|
|
11002
|
+
if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
11003
|
+
restore();
|
|
11004
|
+
reject(abortError());
|
|
11005
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
11006
|
+
restore();
|
|
11007
|
+
resolve(current);
|
|
11008
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
11009
|
+
current = (current + count - 1) % count;
|
|
11010
|
+
block.draw(render(current));
|
|
11011
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
11012
|
+
current = (current + 1) % count;
|
|
11013
|
+
block.draw(render(current));
|
|
11014
|
+
}
|
|
11015
|
+
};
|
|
11016
|
+
const onEnd = () => {
|
|
11017
|
+
restore();
|
|
11018
|
+
reject(abortError());
|
|
11019
|
+
};
|
|
11020
|
+
emitKeypressEvents(input);
|
|
11021
|
+
input.setRawMode(true);
|
|
11022
|
+
input.resume();
|
|
11023
|
+
process.stdout.write("\x1B[?25l");
|
|
11024
|
+
process.on("exit", showCursor);
|
|
11025
|
+
input.on("keypress", onKey);
|
|
11026
|
+
input.on("end", onEnd);
|
|
11027
|
+
});
|
|
11028
|
+
}
|
|
11029
|
+
function showCursor() {
|
|
11030
|
+
process.stdout.write("\x1B[?25h");
|
|
11031
|
+
}
|
|
11032
|
+
|
|
10133
11033
|
// src/init.ts
|
|
10134
11034
|
var SKILLS_ARGV = [
|
|
10135
11035
|
"skills",
|
|
@@ -10146,18 +11046,23 @@ function skillsDir(scope) {
|
|
|
10146
11046
|
if (scope === "local")
|
|
10147
11047
|
return ".claude/skills/";
|
|
10148
11048
|
const config = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
10149
|
-
return config ? `${
|
|
11049
|
+
return config ? `${join10(config, "skills")}/` : "~/.claude/skills/";
|
|
10150
11050
|
}
|
|
10151
11051
|
function template(product) {
|
|
10152
11052
|
return `# firegoat.yaml: evals for how well coding agents use ${product}.
|
|
10153
|
-
# Each test runs your claude in an empty folder
|
|
10154
|
-
# as the first message, then runs \`checks\`
|
|
11053
|
+
# Each test runs your agent (claude by default) in an empty folder
|
|
11054
|
+
# (.firegoat/runs/...) with \`prompt\` as the first message, then runs \`checks\`
|
|
11055
|
+
# there. Reference: npx firegoat init --help
|
|
10155
11056
|
|
|
10156
11057
|
# How many times to run each test.
|
|
10157
11058
|
runs: 1
|
|
10158
11059
|
|
|
10159
|
-
# Optional:
|
|
10160
|
-
#
|
|
11060
|
+
# Optional: the agent CLI each test runs on: claude-code (default), codex-cli or grok-cli.
|
|
11061
|
+
# A list runs every test \`runs\` times per harness. eval --harness NAME overrides it.
|
|
11062
|
+
# harness: [claude-code, codex-cli]
|
|
11063
|
+
|
|
11064
|
+
# Optional: the harness's --model. A list runs every test \`runs\` times per model.
|
|
11065
|
+
# Unset: the harness's default. eval --model NAME overrides it.
|
|
10161
11066
|
# model: [claude-haiku-5-5, claude-sonnet-5-5]
|
|
10162
11067
|
|
|
10163
11068
|
# Optional: test your local build instead of the published package.
|
|
@@ -10204,12 +11109,18 @@ runs: 1
|
|
|
10204
11109
|
# MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
10205
11110
|
# UV_FIND_LINKS: "$FIREGOAT_OUT"
|
|
10206
11111
|
|
|
11112
|
+
# Optional: values for {{vars.KEY}} in prompts, checks and build/install/cleanup
|
|
11113
|
+
# steps (pasted in as-is, unquoted); eval --var KEY=value overrides them. Steps
|
|
11114
|
+
# and checks also get each one as an env var ("$KEY": safe for any value).
|
|
11115
|
+
# vars:
|
|
11116
|
+
# ACCOUNT: acme-test
|
|
11117
|
+
|
|
10207
11118
|
tests:
|
|
10208
11119
|
# A smoke test that firegoat and claude work. Replace it with tests for your
|
|
10209
11120
|
# product: the firegoat-onboarding skill writes them.
|
|
10210
11121
|
- name: goat
|
|
10211
11122
|
# What a user would ask their agent.
|
|
10212
|
-
prompt: Who's the GOAT? Answer in one short sentence.
|
|
11123
|
+
prompt: "Who's the GOAT? Hint: it's me, the person typing this. Answer in one short sentence with the word GOAT in it."
|
|
10213
11124
|
# True when the user's problem is solved. Not graded by firegoat: it goes to
|
|
10214
11125
|
# your agent (eval --json, run.json) to judge.
|
|
10215
11126
|
end_goal: Claude answers that you (the user) are the goat, e.g. YOU'RE THE GOAT.
|
|
@@ -10218,7 +11129,7 @@ tests:
|
|
|
10218
11129
|
checks:
|
|
10219
11130
|
- run: >-
|
|
10220
11131
|
test -s "$FIREGOAT_TRANSCRIPT" &&
|
|
10221
|
-
grep -Eiq '^agent: .*goat' "$FIREGOAT_TRANSCRIPT"
|
|
11132
|
+
grep -Eiq '^agent: .*(you.*goat|goat.*you)' "$FIREGOAT_TRANSCRIPT"
|
|
10222
11133
|
# Seconds before the agent run is stopped.
|
|
10223
11134
|
timeout: 600
|
|
10224
11135
|
`;
|
|
@@ -10232,65 +11143,119 @@ var CLIPBOARD = [
|
|
|
10232
11143
|
];
|
|
10233
11144
|
function copyToClipboard(text) {
|
|
10234
11145
|
return CLIPBOARD.some(([cmd, args]) => {
|
|
10235
|
-
const r =
|
|
11146
|
+
const r = spawnSync4(cmd, args, { input: text, env: hostEnv() });
|
|
10236
11147
|
return !r.error && r.status === 0;
|
|
10237
11148
|
});
|
|
10238
11149
|
}
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
11150
|
+
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
11151
|
+
function cut(s, n) {
|
|
11152
|
+
return s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}~`;
|
|
11153
|
+
}
|
|
11154
|
+
|
|
11155
|
+
class Timeline {
|
|
11156
|
+
paint;
|
|
11157
|
+
width;
|
|
11158
|
+
static INDENT = " ".repeat(9);
|
|
11159
|
+
constructor(paint, width) {
|
|
11160
|
+
this.paint = paint;
|
|
11161
|
+
this.width = width;
|
|
11162
|
+
}
|
|
11163
|
+
line(mark, n, label, status = "") {
|
|
11164
|
+
const head = ` ${mark} ${this.paint("90", String(n).padStart(2, "0"))} `;
|
|
11165
|
+
if (!status)
|
|
11166
|
+
return head + cut(label, this.width - 9);
|
|
11167
|
+
return `${head}${label.padEnd(28)}${this.paint("90", cut(status, this.width - 37))}`;
|
|
11168
|
+
}
|
|
11169
|
+
done(n, label, status) {
|
|
11170
|
+
return this.line(this.paint("32", "●"), n, label, status);
|
|
11171
|
+
}
|
|
11172
|
+
failed(n, label) {
|
|
11173
|
+
return this.line(this.paint("31", "●"), n, label, "Failed");
|
|
11174
|
+
}
|
|
11175
|
+
busy(spin, n, label, status) {
|
|
11176
|
+
return this.line(this.paint("38;5;208", spin), n, label, status);
|
|
11177
|
+
}
|
|
11178
|
+
detail(text) {
|
|
11179
|
+
return wrap(text, Timeline.INDENT, this.width).map((l) => this.paint("90", l));
|
|
11180
|
+
}
|
|
11181
|
+
gap() {
|
|
11182
|
+
console.log(this.paint("90", " │"));
|
|
11183
|
+
}
|
|
11184
|
+
ask(block, n, question, options) {
|
|
11185
|
+
const cols = Math.max(...options.map(([l]) => l.length)) + 3;
|
|
11186
|
+
const room = this.width - Timeline.INDENT.length - 2;
|
|
11187
|
+
return select(block, options.length, (current) => [
|
|
11188
|
+
this.line(this.paint("38;5;208", "◉"), n, question),
|
|
11189
|
+
...options.map(([label, note], i) => {
|
|
11190
|
+
const text = cut(`${label.padEnd(cols)}${note}`, room);
|
|
11191
|
+
return i === current ? `${Timeline.INDENT}${this.paint("38;5;208", `● ${text}`)}` : this.paint("90", `${Timeline.INDENT}○ ${text}`);
|
|
11192
|
+
}),
|
|
11193
|
+
this.paint("90", `${Timeline.INDENT}Up/down to move, enter to pick`)
|
|
11194
|
+
]);
|
|
10245
11195
|
}
|
|
10246
11196
|
}
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
}
|
|
11197
|
+
function runQuiet(cmd, args, opts) {
|
|
11198
|
+
return new Promise((resolve) => {
|
|
11199
|
+
const child = spawn3(cmd, args, {
|
|
11200
|
+
...opts,
|
|
11201
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
11202
|
+
});
|
|
11203
|
+
let output = "";
|
|
11204
|
+
const collect = (d) => {
|
|
11205
|
+
output += d;
|
|
11206
|
+
};
|
|
11207
|
+
child.stdout.on("data", collect);
|
|
11208
|
+
child.stderr.on("data", collect);
|
|
11209
|
+
child.on("error", (error) => resolve({ status: null, output, error }));
|
|
11210
|
+
child.on("close", (status) => resolve({ status, output }));
|
|
11211
|
+
});
|
|
10262
11212
|
}
|
|
10263
|
-
|
|
10264
|
-
async function loginStep(interactive, done, paint) {
|
|
11213
|
+
async function loginStep(ui) {
|
|
10265
11214
|
const label = "firegoat.ai login";
|
|
10266
|
-
const uploadsOn = () => console.log(` ${paint("90", UPLOADS_ON)}`);
|
|
10267
11215
|
const key = resolveKey(baseUrl());
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
uploadsOn();
|
|
10273
|
-
} else
|
|
10274
|
-
console.log(`${who}
|
|
10275
|
-
${UPLOADS_ON}`);
|
|
11216
|
+
const who = key ? key.source === "env" ? "FIREGOAT_API_KEY is set" : `Logged in as ${key.email}` : undefined;
|
|
11217
|
+
if (!ui) {
|
|
11218
|
+
console.log(who ? `${who}
|
|
11219
|
+
${UPLOADS_ON}` : LOGIN_HINT);
|
|
10276
11220
|
return EXIT.pass;
|
|
10277
11221
|
}
|
|
10278
|
-
|
|
10279
|
-
|
|
11222
|
+
const block = liveBlock();
|
|
11223
|
+
if (who) {
|
|
11224
|
+
block.draw([ui.done(4, label, who), ...ui.detail(UPLOADS_ON)]);
|
|
10280
11225
|
return EXIT.pass;
|
|
10281
11226
|
}
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
11227
|
+
const picked = await ui.ask(block, 4, "Login to firegoat.ai to upload runs?", [
|
|
11228
|
+
["Yes", "Opens firegoat.ai in your browser"],
|
|
11229
|
+
["Not now", "Runs stay local"]
|
|
11230
|
+
]);
|
|
11231
|
+
if (picked !== 0) {
|
|
11232
|
+
block.draw([ui.done(4, label, "Skipped (npx firegoat login later)")]);
|
|
10286
11233
|
return EXIT.pass;
|
|
10287
11234
|
}
|
|
11235
|
+
let status = "Connecting";
|
|
11236
|
+
let details = [];
|
|
11237
|
+
const stop = animate(block, (spin) => [
|
|
11238
|
+
ui.busy(spin, 4, label, status),
|
|
11239
|
+
...details
|
|
11240
|
+
]);
|
|
10288
11241
|
try {
|
|
10289
|
-
const email = await loginFlow((
|
|
10290
|
-
|
|
10291
|
-
|
|
11242
|
+
const email = await loginFlow(({ page, code, minutes }) => {
|
|
11243
|
+
status = "Waiting for the browser";
|
|
11244
|
+
details = [
|
|
11245
|
+
...ui.detail(TERMS),
|
|
11246
|
+
...ui.detail(`Open ${page}`),
|
|
11247
|
+
...ui.detail(`Code: ${code} (expires in ${minutes}m)`)
|
|
11248
|
+
];
|
|
11249
|
+
}, true);
|
|
11250
|
+
stop();
|
|
11251
|
+
block.draw([
|
|
11252
|
+
ui.done(4, label, `Logged in as ${email}`),
|
|
11253
|
+
...ui.detail(UPLOADS_ON)
|
|
11254
|
+
]);
|
|
10292
11255
|
return EXIT.pass;
|
|
10293
11256
|
} catch (e) {
|
|
11257
|
+
stop();
|
|
11258
|
+
block.draw([ui.failed(4, label), ...details]);
|
|
10294
11259
|
if (!(e instanceof ApiError))
|
|
10295
11260
|
throw e;
|
|
10296
11261
|
failWith(e instanceof NetworkError ? "network" : "login_failed");
|
|
@@ -10336,84 +11301,93 @@ ${INIT_HELP}`);
|
|
|
10336
11301
|
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
10337
11302
|
const width = Math.min(process.stdout.columns || 80, 80);
|
|
10338
11303
|
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
10339
|
-
const
|
|
10340
|
-
if (
|
|
11304
|
+
const ui = interactive ? new Timeline(paint, width) : undefined;
|
|
11305
|
+
if (ui) {
|
|
10341
11306
|
for (const line of logoHeader(paint, "firegoat init", paint("90", "Get agent-ready."), width))
|
|
10342
11307
|
console.log(line);
|
|
10343
11308
|
console.log(rule);
|
|
10344
11309
|
}
|
|
10345
11310
|
ensureFiregoatDir(cwd);
|
|
10346
|
-
const configPath =
|
|
10347
|
-
const keep =
|
|
11311
|
+
const configPath = join10(cwd, CONFIG_FILE);
|
|
11312
|
+
const keep = existsSync6(configPath) && !force;
|
|
10348
11313
|
if (!keep)
|
|
10349
11314
|
writeFileSync5(configPath, template(basename4(cwd)));
|
|
10350
|
-
if (
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
11315
|
+
if (ui) {
|
|
11316
|
+
console.log(ui.done(1, CONFIG_FILE, keep ? "Kept (--force replaces it)" : "Created"));
|
|
11317
|
+
ui.gap();
|
|
11318
|
+
console.log(ui.done(2, ".firegoat/", "Ready"));
|
|
10354
11319
|
} else
|
|
10355
11320
|
console.log(keep ? `Kept ${CONFIG_FILE} (--force replaces it)` : `Wrote ${CONFIG_FILE}: replace the goat smoke test with tests for your product`);
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
11321
|
+
const skill = "firegoat-onboarding skill";
|
|
11322
|
+
const block = liveBlock();
|
|
11323
|
+
ui?.gap();
|
|
11324
|
+
if (ui && !noSkills && !localSkills) {
|
|
11325
|
+
const picked = await ui.ask(block, 3, "Install the firegoat-onboarding skill", [
|
|
11326
|
+
["Global", skillsDir("global")],
|
|
11327
|
+
["This repo", `${skillsDir("local")} (commit it for your team)`],
|
|
11328
|
+
["Skip", "Don't install it"]
|
|
11329
|
+
]);
|
|
11330
|
+
scope = ["global", "local", "none"][picked] ?? "none";
|
|
10361
11331
|
initSkills(scope);
|
|
10362
11332
|
}
|
|
10363
11333
|
if (scope !== "none") {
|
|
10364
11334
|
const argv = [...SKILLS_ARGV, ...scope === "global" ? ["-g"] : []];
|
|
10365
11335
|
const where = skillsDir(scope);
|
|
10366
|
-
if (!
|
|
11336
|
+
if (!ui)
|
|
10367
11337
|
console.log(`Installing the skill: npx ${argv.join(" ")}`);
|
|
10368
11338
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== "npm_config_package" && !k.startsWith("FIREGOAT_")));
|
|
10369
|
-
const
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
});
|
|
11339
|
+
const stop = ui ? animate(block, (spin) => [
|
|
11340
|
+
ui.busy(spin, 3, skill, `Installing → ${where}`)
|
|
11341
|
+
]) : undefined;
|
|
11342
|
+
const r = await runQuiet("npx", ["-y", ...argv], { cwd, env });
|
|
11343
|
+
stop?.();
|
|
10375
11344
|
if (r.error || r.status !== 0) {
|
|
10376
11345
|
failWith("skills_failed");
|
|
10377
|
-
|
|
11346
|
+
if (ui)
|
|
11347
|
+
block.draw([ui.failed(3, skill)]);
|
|
11348
|
+
process.stderr.write(r.output);
|
|
10378
11349
|
console.error(`firegoat init: Installing the skill failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${argv.join(" ")}`);
|
|
10379
11350
|
return EXIT.notGraded;
|
|
10380
11351
|
}
|
|
10381
|
-
if (
|
|
10382
|
-
|
|
11352
|
+
if (ui)
|
|
11353
|
+
block.draw([ui.done(3, skill, `Installed → ${where}`)]);
|
|
10383
11354
|
else
|
|
10384
11355
|
console.log(`Installed skill firegoat-onboarding in ${where}`);
|
|
10385
|
-
const ignored = scope === "local" &&
|
|
11356
|
+
const ignored = scope === "local" && spawnSync4("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
10386
11357
|
cwd,
|
|
10387
11358
|
env: hostEnv()
|
|
10388
11359
|
}).status === 0;
|
|
10389
11360
|
if (ignored)
|
|
10390
11361
|
console.error("Warning: git ignores .claude/skills, so teammates and CI won't get the skill. Commit them with: git add -f .claude/skills");
|
|
10391
|
-
} else if (
|
|
10392
|
-
|
|
11362
|
+
} else if (ui)
|
|
11363
|
+
block.draw([ui.done(3, skill, "Skipped")]);
|
|
10393
11364
|
if (login) {
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
const loginExit = await loginStep(interactive, (label, status) => step(4, label, status), paint);
|
|
11365
|
+
ui?.gap();
|
|
11366
|
+
const loginExit = await loginStep(ui);
|
|
10397
11367
|
if (loginExit !== EXIT.pass)
|
|
10398
11368
|
return loginExit;
|
|
10399
11369
|
}
|
|
10400
11370
|
const telemetryOn = telemetryEnabled();
|
|
10401
|
-
if (!
|
|
11371
|
+
if (!ui) {
|
|
10402
11372
|
if (telemetryOn)
|
|
10403
11373
|
console.log(TELEMETRY_NOTICE);
|
|
10404
11374
|
console.log(`
|
|
10405
11375
|
Prompt for your coding agent: ${AGENT_PROMPT}`);
|
|
10406
11376
|
return EXIT.pass;
|
|
10407
11377
|
}
|
|
10408
|
-
if (telemetryOn)
|
|
10409
|
-
console.log(` ${paint("90", TELEMETRY_NOTICE)}`);
|
|
10410
11378
|
console.log(rule);
|
|
11379
|
+
console.log("");
|
|
10411
11380
|
console.log(` Next: hand this to your coding agent
|
|
10412
11381
|
`);
|
|
10413
11382
|
console.log(AGENT_PROMPT);
|
|
10414
11383
|
console.log("");
|
|
10415
11384
|
const copied = copyToClipboard(AGENT_PROMPT);
|
|
10416
11385
|
console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}Then: ${paint("36", "npx firegoat@latest eval")}`);
|
|
11386
|
+
if (telemetryOn) {
|
|
11387
|
+
console.log("");
|
|
11388
|
+
for (const l of wrap(TELEMETRY_NOTICE, " ", width))
|
|
11389
|
+
console.log(paint("90", l));
|
|
11390
|
+
}
|
|
10417
11391
|
return EXIT.pass;
|
|
10418
11392
|
}
|
|
10419
11393
|
|