firegoat 0.29.0 → 0.31.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 +3 -1
- package/dist/cli.js +562 -294
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ So far we've got the CLI. It lets anyone use a simplified version of our product
|
|
|
8
8
|
|
|
9
9
|
## What you get
|
|
10
10
|
|
|
11
|
-
When you run `firegoat eval`, you get visibility into how agents use your product: whether they fail or succeed, and how much time they take doing it. You can point them at your local codebase or at your cloud product, and see how Claude acts on the simple tasks your product aims to do.
|
|
11
|
+
When you run `firegoat eval`, you get visibility into how agents use your product: whether they fail or succeed, and how much time and how many tokens they take doing it. You can point them at your local codebase or at your cloud product, and see how Claude acts on the simple tasks your product aims to do. Give harder tests more `reward_points` and eval tells you the tokens spent per point earned, per harness and model.
|
|
12
12
|
|
|
13
13
|
## Start
|
|
14
14
|
|
|
@@ -22,6 +22,8 @@ In a terminal it asks where to install the firegoat-onboarding skill (global by
|
|
|
22
22
|
npx firegoat@latest eval
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Run by an agent, eval ends with a results block to paste as-is (also in `.firegoat/runs/<runId>/results.txt`): ask it to show you the results table.
|
|
26
|
+
|
|
25
27
|
## Example
|
|
26
28
|
|
|
27
29
|
```yaml
|
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.31.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7039,6 +7039,230 @@ var $visitAsync = visit.visitAsync;
|
|
|
7039
7039
|
import { spawnSync } from "node:child_process";
|
|
7040
7040
|
import { existsSync, readdirSync } from "node:fs";
|
|
7041
7041
|
import { join } from "node:path";
|
|
7042
|
+
|
|
7043
|
+
// src/process.ts
|
|
7044
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7045
|
+
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7046
|
+
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)$/;
|
|
7047
|
+
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7048
|
+
function hostEnv() {
|
|
7049
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7050
|
+
}
|
|
7051
|
+
var GRACE_MS = 5000;
|
|
7052
|
+
var liveGroups = new Set;
|
|
7053
|
+
var signalsHooked = false;
|
|
7054
|
+
function hookSignals() {
|
|
7055
|
+
if (signalsHooked)
|
|
7056
|
+
return;
|
|
7057
|
+
signalsHooked = true;
|
|
7058
|
+
const onSignal = (signal) => {
|
|
7059
|
+
for (const g of liveGroups)
|
|
7060
|
+
try {
|
|
7061
|
+
process.kill(-g, "SIGKILL");
|
|
7062
|
+
} catch {}
|
|
7063
|
+
process.kill(process.pid, signal);
|
|
7064
|
+
};
|
|
7065
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
7066
|
+
process.once(s, onSignal);
|
|
7067
|
+
}
|
|
7068
|
+
function forgetGroup(pgid) {
|
|
7069
|
+
liveGroups.delete(pgid);
|
|
7070
|
+
}
|
|
7071
|
+
function processTree(pid) {
|
|
7072
|
+
const children = new Map;
|
|
7073
|
+
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
7074
|
+
encoding: "utf8"
|
|
7075
|
+
});
|
|
7076
|
+
for (const line of ps.split(`
|
|
7077
|
+
`)) {
|
|
7078
|
+
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
7079
|
+
if (!child || parent === undefined)
|
|
7080
|
+
continue;
|
|
7081
|
+
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
7082
|
+
}
|
|
7083
|
+
const tree = [pid];
|
|
7084
|
+
for (let i = 0;i < tree.length; i++)
|
|
7085
|
+
tree.push(...children.get(tree[i]) ?? []);
|
|
7086
|
+
return tree;
|
|
7087
|
+
}
|
|
7088
|
+
function killTree(pid, group) {
|
|
7089
|
+
if (group)
|
|
7090
|
+
try {
|
|
7091
|
+
process.kill(-pid, "SIGKILL");
|
|
7092
|
+
} catch {}
|
|
7093
|
+
let tree = [pid];
|
|
7094
|
+
let problem;
|
|
7095
|
+
try {
|
|
7096
|
+
tree = processTree(pid);
|
|
7097
|
+
} catch (e) {
|
|
7098
|
+
problem = e.message;
|
|
7099
|
+
}
|
|
7100
|
+
for (const p of tree)
|
|
7101
|
+
try {
|
|
7102
|
+
process.kill(p, "SIGKILL");
|
|
7103
|
+
} catch {}
|
|
7104
|
+
return problem;
|
|
7105
|
+
}
|
|
7106
|
+
function run(opts) {
|
|
7107
|
+
return new Promise((resolve) => {
|
|
7108
|
+
const detached = opts.detached === true;
|
|
7109
|
+
if (detached)
|
|
7110
|
+
hookSignals();
|
|
7111
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
7112
|
+
cwd: opts.cwd,
|
|
7113
|
+
env: opts.env,
|
|
7114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7115
|
+
detached
|
|
7116
|
+
});
|
|
7117
|
+
if (child.pid !== undefined) {
|
|
7118
|
+
if (detached)
|
|
7119
|
+
liveGroups.add(child.pid);
|
|
7120
|
+
opts.onSpawn?.(child.pid);
|
|
7121
|
+
}
|
|
7122
|
+
let timedOut = false;
|
|
7123
|
+
let settled = false;
|
|
7124
|
+
const timer = setTimeout(() => {
|
|
7125
|
+
timedOut = true;
|
|
7126
|
+
if (child.pid === undefined)
|
|
7127
|
+
return;
|
|
7128
|
+
const problem = killTree(child.pid, detached);
|
|
7129
|
+
if (problem)
|
|
7130
|
+
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
7131
|
+
`));
|
|
7132
|
+
}, opts.timeoutMs);
|
|
7133
|
+
const done = (r) => {
|
|
7134
|
+
if (settled)
|
|
7135
|
+
return;
|
|
7136
|
+
settled = true;
|
|
7137
|
+
clearTimeout(timer);
|
|
7138
|
+
resolve(r);
|
|
7139
|
+
};
|
|
7140
|
+
child.stdout.on("data", opts.onStdout);
|
|
7141
|
+
child.stderr.on("data", opts.onStderr);
|
|
7142
|
+
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
7143
|
+
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
7144
|
+
child.on("close", (code) => done({ code, timedOut }));
|
|
7145
|
+
child.on("exit", (code) => setTimeout(() => {
|
|
7146
|
+
child.stdout.destroy();
|
|
7147
|
+
child.stderr.destroy();
|
|
7148
|
+
done({ code, timedOut });
|
|
7149
|
+
}, 2000).unref());
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7152
|
+
async function runShell(opts) {
|
|
7153
|
+
opts.log.write(`$ ${opts.run}
|
|
7154
|
+
`);
|
|
7155
|
+
const r = await run({
|
|
7156
|
+
cmd: "/bin/sh",
|
|
7157
|
+
args: ["-c", opts.run],
|
|
7158
|
+
cwd: opts.cwd,
|
|
7159
|
+
env: opts.env,
|
|
7160
|
+
timeoutMs: opts.timeoutMs,
|
|
7161
|
+
onStdout: (c) => opts.log.write(c),
|
|
7162
|
+
onStderr: (c) => opts.log.write(c)
|
|
7163
|
+
});
|
|
7164
|
+
let error;
|
|
7165
|
+
if (r.error)
|
|
7166
|
+
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7167
|
+
else if (r.timedOut)
|
|
7168
|
+
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7169
|
+
else if (r.code === null)
|
|
7170
|
+
error = "killed by a signal";
|
|
7171
|
+
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7172
|
+
|
|
7173
|
+
`);
|
|
7174
|
+
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7175
|
+
}
|
|
7176
|
+
|
|
7177
|
+
// src/harness.ts
|
|
7178
|
+
function tokens(parts = {
|
|
7179
|
+
input: null,
|
|
7180
|
+
output: null,
|
|
7181
|
+
cacheRead: null,
|
|
7182
|
+
cacheWrite: null
|
|
7183
|
+
}) {
|
|
7184
|
+
const { input, output, cacheRead, cacheWrite } = parts;
|
|
7185
|
+
const total = input === null || output === null || cacheRead === null || cacheWrite === null ? null : input + output + cacheRead + cacheWrite;
|
|
7186
|
+
return { input, output, cacheRead, cacheWrite, total };
|
|
7187
|
+
}
|
|
7188
|
+
function modelUsageTokens(modelUsage) {
|
|
7189
|
+
if (typeof modelUsage !== "object" || modelUsage === null)
|
|
7190
|
+
return tokens();
|
|
7191
|
+
const entries = Object.values(modelUsage);
|
|
7192
|
+
if (entries.length === 0)
|
|
7193
|
+
return tokens();
|
|
7194
|
+
const sum = (key) => {
|
|
7195
|
+
let n = 0;
|
|
7196
|
+
for (const e of entries) {
|
|
7197
|
+
const v = e?.[key];
|
|
7198
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0)
|
|
7199
|
+
return null;
|
|
7200
|
+
n += v;
|
|
7201
|
+
}
|
|
7202
|
+
return n;
|
|
7203
|
+
};
|
|
7204
|
+
return tokens({
|
|
7205
|
+
input: sum("inputTokens"),
|
|
7206
|
+
output: sum("outputTokens"),
|
|
7207
|
+
cacheRead: sum("cacheReadInputTokens"),
|
|
7208
|
+
cacheWrite: sum("cacheCreationInputTokens")
|
|
7209
|
+
});
|
|
7210
|
+
}
|
|
7211
|
+
async function runAgent(harness, opts) {
|
|
7212
|
+
let stdout = "";
|
|
7213
|
+
let pending = "";
|
|
7214
|
+
let group;
|
|
7215
|
+
let markFinal = () => {};
|
|
7216
|
+
const finalSeen = new Promise((r) => {
|
|
7217
|
+
markFinal = r;
|
|
7218
|
+
});
|
|
7219
|
+
const exited = await run({
|
|
7220
|
+
cmd: opts.bin,
|
|
7221
|
+
args: harness.args(opts),
|
|
7222
|
+
cwd: opts.cwd,
|
|
7223
|
+
env: opts.env,
|
|
7224
|
+
timeoutMs: opts.timeoutMs,
|
|
7225
|
+
detached: true,
|
|
7226
|
+
finalSeen,
|
|
7227
|
+
onSpawn: (pid) => {
|
|
7228
|
+
group = pid;
|
|
7229
|
+
},
|
|
7230
|
+
onStdout: (c) => {
|
|
7231
|
+
const text = c.toString();
|
|
7232
|
+
stdout += text;
|
|
7233
|
+
pending += text;
|
|
7234
|
+
const lines = pending.split(`
|
|
7235
|
+
`);
|
|
7236
|
+
pending = lines.pop() ?? "";
|
|
7237
|
+
for (const line of lines) {
|
|
7238
|
+
if (harness.isFinal(line))
|
|
7239
|
+
markFinal();
|
|
7240
|
+
const p = opts.onProgress && harness.progress(line);
|
|
7241
|
+
if (p)
|
|
7242
|
+
opts.onProgress?.(p);
|
|
7243
|
+
}
|
|
7244
|
+
},
|
|
7245
|
+
onStderr: (c) => opts.stderr.write(c)
|
|
7246
|
+
});
|
|
7247
|
+
if (exited.error)
|
|
7248
|
+
return {
|
|
7249
|
+
group,
|
|
7250
|
+
outcome: {
|
|
7251
|
+
ok: false,
|
|
7252
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
7253
|
+
steps: [],
|
|
7254
|
+
final: "",
|
|
7255
|
+
reportedModel: null,
|
|
7256
|
+
version: null,
|
|
7257
|
+
turns: null,
|
|
7258
|
+
cost: null,
|
|
7259
|
+
tokens: tokens()
|
|
7260
|
+
}
|
|
7261
|
+
};
|
|
7262
|
+
return { group, outcome: harness.read(stdout, exited, opts.timeoutMs) };
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
// src/harnesses/claude.ts
|
|
7042
7266
|
var FLAGS = [
|
|
7043
7267
|
"--output-format",
|
|
7044
7268
|
"stream-json",
|
|
@@ -7050,6 +7274,17 @@ var FLAGS = [
|
|
|
7050
7274
|
"--disable-slash-commands"
|
|
7051
7275
|
];
|
|
7052
7276
|
var SET = "firegoat sets it for every run";
|
|
7277
|
+
function sumTurns(results) {
|
|
7278
|
+
if (results.length === 0)
|
|
7279
|
+
return null;
|
|
7280
|
+
let n = 0;
|
|
7281
|
+
for (const r of results) {
|
|
7282
|
+
if (typeof r.num_turns !== "number")
|
|
7283
|
+
return null;
|
|
7284
|
+
n += r.num_turns;
|
|
7285
|
+
}
|
|
7286
|
+
return n;
|
|
7287
|
+
}
|
|
7053
7288
|
function parse(line) {
|
|
7054
7289
|
try {
|
|
7055
7290
|
const e = JSON.parse(line);
|
|
@@ -7105,12 +7340,14 @@ function read(stdout, exited, timeoutMs) {
|
|
|
7105
7340
|
for (const e of events)
|
|
7106
7341
|
if (e?.type === "result" && typeof e.result === "string")
|
|
7107
7342
|
final = e.result;
|
|
7108
|
-
const
|
|
7343
|
+
const results = events.filter((e) => e?.type === "result");
|
|
7344
|
+
const result = results.at(-1);
|
|
7109
7345
|
const trace = {
|
|
7110
7346
|
steps: steps(events),
|
|
7111
7347
|
final,
|
|
7112
|
-
turns:
|
|
7113
|
-
cost: result?.total_cost_usd ?? null
|
|
7348
|
+
turns: sumTurns(results),
|
|
7349
|
+
cost: result?.total_cost_usd ?? null,
|
|
7350
|
+
tokens: modelUsageTokens(result?.modelUsage)
|
|
7114
7351
|
};
|
|
7115
7352
|
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7116
7353
|
if (!init) {
|
|
@@ -7269,142 +7506,6 @@ import { execFile } from "node:child_process";
|
|
|
7269
7506
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7270
7507
|
import { join as join2 } from "node:path";
|
|
7271
7508
|
import { promisify } from "node:util";
|
|
7272
|
-
|
|
7273
|
-
// src/process.ts
|
|
7274
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
7275
|
-
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7276
|
-
var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
|
|
7277
|
-
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7278
|
-
function hostEnv() {
|
|
7279
|
-
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7280
|
-
}
|
|
7281
|
-
var GRACE_MS = 5000;
|
|
7282
|
-
var liveGroups = new Set;
|
|
7283
|
-
var signalsHooked = false;
|
|
7284
|
-
function hookSignals() {
|
|
7285
|
-
if (signalsHooked)
|
|
7286
|
-
return;
|
|
7287
|
-
signalsHooked = true;
|
|
7288
|
-
const onSignal = (signal) => {
|
|
7289
|
-
for (const g of liveGroups)
|
|
7290
|
-
try {
|
|
7291
|
-
process.kill(-g, "SIGKILL");
|
|
7292
|
-
} catch {}
|
|
7293
|
-
process.kill(process.pid, signal);
|
|
7294
|
-
};
|
|
7295
|
-
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
7296
|
-
process.once(s, onSignal);
|
|
7297
|
-
}
|
|
7298
|
-
function forgetGroup(pgid) {
|
|
7299
|
-
liveGroups.delete(pgid);
|
|
7300
|
-
}
|
|
7301
|
-
function processTree(pid) {
|
|
7302
|
-
const children = new Map;
|
|
7303
|
-
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
7304
|
-
encoding: "utf8"
|
|
7305
|
-
});
|
|
7306
|
-
for (const line of ps.split(`
|
|
7307
|
-
`)) {
|
|
7308
|
-
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
7309
|
-
if (!child || parent === undefined)
|
|
7310
|
-
continue;
|
|
7311
|
-
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
7312
|
-
}
|
|
7313
|
-
const tree = [pid];
|
|
7314
|
-
for (let i = 0;i < tree.length; i++)
|
|
7315
|
-
tree.push(...children.get(tree[i]) ?? []);
|
|
7316
|
-
return tree;
|
|
7317
|
-
}
|
|
7318
|
-
function killTree(pid, group) {
|
|
7319
|
-
if (group)
|
|
7320
|
-
try {
|
|
7321
|
-
process.kill(-pid, "SIGKILL");
|
|
7322
|
-
} catch {}
|
|
7323
|
-
let tree = [pid];
|
|
7324
|
-
let problem;
|
|
7325
|
-
try {
|
|
7326
|
-
tree = processTree(pid);
|
|
7327
|
-
} catch (e) {
|
|
7328
|
-
problem = e.message;
|
|
7329
|
-
}
|
|
7330
|
-
for (const p of tree)
|
|
7331
|
-
try {
|
|
7332
|
-
process.kill(p, "SIGKILL");
|
|
7333
|
-
} catch {}
|
|
7334
|
-
return problem;
|
|
7335
|
-
}
|
|
7336
|
-
function run(opts) {
|
|
7337
|
-
return new Promise((resolve) => {
|
|
7338
|
-
const detached = opts.detached === true;
|
|
7339
|
-
if (detached)
|
|
7340
|
-
hookSignals();
|
|
7341
|
-
const child = spawn(opts.cmd, opts.args, {
|
|
7342
|
-
cwd: opts.cwd,
|
|
7343
|
-
env: opts.env,
|
|
7344
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
7345
|
-
detached
|
|
7346
|
-
});
|
|
7347
|
-
if (child.pid !== undefined) {
|
|
7348
|
-
if (detached)
|
|
7349
|
-
liveGroups.add(child.pid);
|
|
7350
|
-
opts.onSpawn?.(child.pid);
|
|
7351
|
-
}
|
|
7352
|
-
let timedOut = false;
|
|
7353
|
-
let settled = false;
|
|
7354
|
-
const timer = setTimeout(() => {
|
|
7355
|
-
timedOut = true;
|
|
7356
|
-
if (child.pid === undefined)
|
|
7357
|
-
return;
|
|
7358
|
-
const problem = killTree(child.pid, detached);
|
|
7359
|
-
if (problem)
|
|
7360
|
-
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
7361
|
-
`));
|
|
7362
|
-
}, opts.timeoutMs);
|
|
7363
|
-
const done = (r) => {
|
|
7364
|
-
if (settled)
|
|
7365
|
-
return;
|
|
7366
|
-
settled = true;
|
|
7367
|
-
clearTimeout(timer);
|
|
7368
|
-
resolve(r);
|
|
7369
|
-
};
|
|
7370
|
-
child.stdout.on("data", opts.onStdout);
|
|
7371
|
-
child.stderr.on("data", opts.onStderr);
|
|
7372
|
-
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
7373
|
-
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
7374
|
-
child.on("close", (code) => done({ code, timedOut }));
|
|
7375
|
-
child.on("exit", (code) => setTimeout(() => {
|
|
7376
|
-
child.stdout.destroy();
|
|
7377
|
-
child.stderr.destroy();
|
|
7378
|
-
done({ code, timedOut });
|
|
7379
|
-
}, 2000).unref());
|
|
7380
|
-
});
|
|
7381
|
-
}
|
|
7382
|
-
async function runShell(opts) {
|
|
7383
|
-
opts.log.write(`$ ${opts.run}
|
|
7384
|
-
`);
|
|
7385
|
-
const r = await run({
|
|
7386
|
-
cmd: "/bin/sh",
|
|
7387
|
-
args: ["-c", opts.run],
|
|
7388
|
-
cwd: opts.cwd,
|
|
7389
|
-
env: opts.env,
|
|
7390
|
-
timeoutMs: opts.timeoutMs,
|
|
7391
|
-
onStdout: (c) => opts.log.write(c),
|
|
7392
|
-
onStderr: (c) => opts.log.write(c)
|
|
7393
|
-
});
|
|
7394
|
-
let error;
|
|
7395
|
-
if (r.error)
|
|
7396
|
-
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7397
|
-
else if (r.timedOut)
|
|
7398
|
-
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7399
|
-
else if (r.code === null)
|
|
7400
|
-
error = "killed by a signal";
|
|
7401
|
-
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7402
|
-
|
|
7403
|
-
`);
|
|
7404
|
-
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7405
|
-
}
|
|
7406
|
-
|
|
7407
|
-
// src/home.ts
|
|
7408
7509
|
function homeEnv(home) {
|
|
7409
7510
|
return {
|
|
7410
7511
|
HOME: home,
|
|
@@ -7551,6 +7652,33 @@ function errorMessage(raw) {
|
|
|
7551
7652
|
} catch {}
|
|
7552
7653
|
return raw;
|
|
7553
7654
|
}
|
|
7655
|
+
function usageTokens(events) {
|
|
7656
|
+
const usages = events.filter((e) => e?.type === "turn.completed").map((e) => e?.usage ?? {});
|
|
7657
|
+
if (usages.length === 0)
|
|
7658
|
+
return tokens();
|
|
7659
|
+
const sum = (key) => {
|
|
7660
|
+
let n = 0;
|
|
7661
|
+
for (const u of usages) {
|
|
7662
|
+
const v = u[key];
|
|
7663
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0)
|
|
7664
|
+
return null;
|
|
7665
|
+
n += v;
|
|
7666
|
+
}
|
|
7667
|
+
return n;
|
|
7668
|
+
};
|
|
7669
|
+
const input = sum("input_tokens");
|
|
7670
|
+
const cacheRead = sum("cached_input_tokens");
|
|
7671
|
+
const cacheWrite = sum("cache_write_input_tokens");
|
|
7672
|
+
const uncached = input === null || cacheRead === null || cacheWrite === null ? null : input - cacheRead - cacheWrite;
|
|
7673
|
+
if (uncached !== null && uncached < 0)
|
|
7674
|
+
throw new Error(`codex reported input_tokens ${input} below cached_input_tokens ${cacheRead} + cache_write_input_tokens ${cacheWrite}: its usage is no longer what firegoat reads (src/harnesses/codex.ts usageTokens)`);
|
|
7675
|
+
return tokens({
|
|
7676
|
+
input: uncached,
|
|
7677
|
+
output: sum("output_tokens"),
|
|
7678
|
+
cacheRead,
|
|
7679
|
+
cacheWrite
|
|
7680
|
+
});
|
|
7681
|
+
}
|
|
7554
7682
|
function read2(stdout, exited, timeoutMs) {
|
|
7555
7683
|
const events = stdout.split(`
|
|
7556
7684
|
`).filter((l) => l.trim() !== "").map(parse2);
|
|
@@ -7560,7 +7688,8 @@ function read2(stdout, exited, timeoutMs) {
|
|
|
7560
7688
|
reportedModel: null,
|
|
7561
7689
|
version: null,
|
|
7562
7690
|
turns: null,
|
|
7563
|
-
cost: null
|
|
7691
|
+
cost: null,
|
|
7692
|
+
tokens: usageTokens(events)
|
|
7564
7693
|
};
|
|
7565
7694
|
const thread = events.find((e) => e?.type === "thread.started");
|
|
7566
7695
|
if (!thread) {
|
|
@@ -7833,7 +7962,8 @@ function read3(stdout, exited, timeoutMs) {
|
|
|
7833
7962
|
steps: steps3(events),
|
|
7834
7963
|
final: typeof result?.result === "string" ? result.result : "",
|
|
7835
7964
|
turns: result?.num_turns ?? null,
|
|
7836
|
-
cost: result?.total_cost_usd ?? null
|
|
7965
|
+
cost: result?.total_cost_usd ?? null,
|
|
7966
|
+
tokens: modelUsageTokens(result?.modelUsage)
|
|
7837
7967
|
};
|
|
7838
7968
|
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7839
7969
|
const reportedModel = typeof init?.model === "string" && init.model !== "unknown" ? init.model : null;
|
|
@@ -8110,6 +8240,7 @@ function parseConfig(text) {
|
|
|
8110
8240
|
"end_goal",
|
|
8111
8241
|
"checks",
|
|
8112
8242
|
"timeout",
|
|
8243
|
+
"reward_points",
|
|
8113
8244
|
"env",
|
|
8114
8245
|
"install",
|
|
8115
8246
|
"cleanup",
|
|
@@ -8140,6 +8271,7 @@ function parseConfig(text) {
|
|
|
8140
8271
|
end_goal: str2(t.end_goal, `${p}.end_goal`),
|
|
8141
8272
|
checks,
|
|
8142
8273
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
8274
|
+
rewardPoints: posInt(t.reward_points, `${p}.reward_points`, 1),
|
|
8143
8275
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
8144
8276
|
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
8145
8277
|
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
@@ -8743,60 +8875,6 @@ function gitRepo(dir) {
|
|
|
8743
8875
|
};
|
|
8744
8876
|
}
|
|
8745
8877
|
|
|
8746
|
-
// src/harness.ts
|
|
8747
|
-
async function runAgent(harness, opts) {
|
|
8748
|
-
let stdout = "";
|
|
8749
|
-
let pending = "";
|
|
8750
|
-
let group;
|
|
8751
|
-
let markFinal = () => {};
|
|
8752
|
-
const finalSeen = new Promise((r) => {
|
|
8753
|
-
markFinal = r;
|
|
8754
|
-
});
|
|
8755
|
-
const exited = await run({
|
|
8756
|
-
cmd: opts.bin,
|
|
8757
|
-
args: harness.args(opts),
|
|
8758
|
-
cwd: opts.cwd,
|
|
8759
|
-
env: opts.env,
|
|
8760
|
-
timeoutMs: opts.timeoutMs,
|
|
8761
|
-
detached: true,
|
|
8762
|
-
finalSeen,
|
|
8763
|
-
onSpawn: (pid) => {
|
|
8764
|
-
group = pid;
|
|
8765
|
-
},
|
|
8766
|
-
onStdout: (c) => {
|
|
8767
|
-
const text = c.toString();
|
|
8768
|
-
stdout += text;
|
|
8769
|
-
pending += text;
|
|
8770
|
-
const lines = pending.split(`
|
|
8771
|
-
`);
|
|
8772
|
-
pending = lines.pop() ?? "";
|
|
8773
|
-
for (const line of lines) {
|
|
8774
|
-
if (harness.isFinal(line))
|
|
8775
|
-
markFinal();
|
|
8776
|
-
const p = opts.onProgress && harness.progress(line);
|
|
8777
|
-
if (p)
|
|
8778
|
-
opts.onProgress?.(p);
|
|
8779
|
-
}
|
|
8780
|
-
},
|
|
8781
|
-
onStderr: (c) => opts.stderr.write(c)
|
|
8782
|
-
});
|
|
8783
|
-
if (exited.error)
|
|
8784
|
-
return {
|
|
8785
|
-
group,
|
|
8786
|
-
outcome: {
|
|
8787
|
-
ok: false,
|
|
8788
|
-
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
8789
|
-
steps: [],
|
|
8790
|
-
final: "",
|
|
8791
|
-
reportedModel: null,
|
|
8792
|
-
version: null,
|
|
8793
|
-
turns: null,
|
|
8794
|
-
cost: null
|
|
8795
|
-
}
|
|
8796
|
-
};
|
|
8797
|
-
return { group, outcome: harness.read(stdout, exited, opts.timeoutMs) };
|
|
8798
|
-
}
|
|
8799
|
-
|
|
8800
8878
|
// src/help.ts
|
|
8801
8879
|
var EXIT_CODES = `Exit codes:
|
|
8802
8880
|
0 Every run's script checks passed
|
|
@@ -8944,6 +9022,8 @@ firegoat.yaml reference:
|
|
|
8944
9022
|
checks: Optional shell commands, run in the agent's folder
|
|
8945
9023
|
- run: ... Afterwards; exit 0 = pass; they set eval's exit code
|
|
8946
9024
|
timeout: 600 Optional, seconds
|
|
9025
|
+
reward_points: 3 Optional positive integer (default 1): what a run
|
|
9026
|
+
earns when its checks pass; harder end goals get more
|
|
8947
9027
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
8948
9028
|
install: <sh> | [...] Optional, runs after the top-level install
|
|
8949
9029
|
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
@@ -9171,7 +9251,8 @@ firegoat.yaml, fixtures and build: stay in the current folder.
|
|
|
9171
9251
|
cleanup); env for build, install, checks and cleanup
|
|
9172
9252
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
9173
9253
|
stops early); progress goes to stderr. The verdict comes
|
|
9174
|
-
last, after runs
|
|
9254
|
+
last, after runs and the reward points (Reward, below):
|
|
9255
|
+
totals {pass, fail, error}, tier
|
|
9175
9256
|
(S/A/B/C, as printed; null when no run was graded), status
|
|
9176
9257
|
("pass", "fail" or "error" = not graded) and exitCode
|
|
9177
9258
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
@@ -9203,17 +9284,45 @@ says "N uploads failed" on stderr. A file over 20 MiB fails that run's upload.
|
|
|
9203
9284
|
Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
9204
9285
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
9205
9286
|
|
|
9287
|
+
Results block: without a terminal (an agent, a pipe, CI) and without --json, eval
|
|
9288
|
+
ends with the terminal's final timeline as one plain block (no color, at most 100
|
|
9289
|
+
columns): the logo header, a row per run (status, a ramp of one block per agent turn
|
|
9290
|
+
ending ▲ pass, ╳ fail or ╌ not graded, checks passed, cost, turns, tokens, time), its
|
|
9291
|
+
failed checks and trace URL, the totals, the tier ("GOATED · tier S" when all pass),
|
|
9292
|
+
"Reward points 7/9 · 12k tokens per point", and with more than one harness × model a
|
|
9293
|
+
table of each one's points, tokens per point, turns and time. It
|
|
9294
|
+
sits between two lines of ═, after a "Results (paste the block ...)" line; the last
|
|
9295
|
+
line says where it is saved, results.txt. Show it to your user as-is in a code block.
|
|
9296
|
+
|
|
9297
|
+
Reward: value per token. A run earns its test's reward_points when its checks pass,
|
|
9298
|
+
else 0; not graded runs earn 0 and still count in the max. tokens = input (uncached)
|
|
9299
|
+
+ output + cacheRead + cacheWrite, as the harness reports them, never estimated:
|
|
9300
|
+
claude-code and grok-cli from the result line's modelUsage (all models, subagents
|
|
9301
|
+
too), codex-cli from turn.completed usage (its cached and cache-written tokens are
|
|
9302
|
+
taken out of input_tokens). Tokens per reward point = the tokens of every run,
|
|
9303
|
+
passed or not, / the points earned; null when no points were earned or any run's
|
|
9304
|
+
tokens are unknown. Tokenizers differ (claude, gpt, grok), so token counts across
|
|
9305
|
+
harnesses are approximate.
|
|
9306
|
+
|
|
9206
9307
|
Output: .firegoat/runs/<runId>/
|
|
9207
9308
|
out A symlink to $FIREGOAT_OUT,
|
|
9208
9309
|
~/.firegoat/runs/<hash>/<random>/out
|
|
9209
9310
|
summary.json firegoatVersion, runId, runsDir, dir (where eval
|
|
9210
9311
|
ran), git {branch, commit, dirty} (no git outside a
|
|
9211
9312
|
repo), harnesses, upload {origin, email} (null when
|
|
9212
|
-
not uploading), runs (every run.json),
|
|
9213
|
-
|
|
9313
|
+
not uploading), runs (every run.json),
|
|
9314
|
+
rewardPoints, rewardPointsMax, tokens (each part
|
|
9315
|
+
summed; null when any run's is), tokensPerRewardPoint
|
|
9316
|
+
(null with 0 points or unknown tokens), groups (per
|
|
9317
|
+
harness × model: harness, model, runs, the same four,
|
|
9318
|
+
turns, durationMs), then the verdict: totals, tier,
|
|
9319
|
+
status and exitCode
|
|
9320
|
+
results.txt The results block, also with --json or in a terminal
|
|
9214
9321
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
9215
9322
|
commit, harnesses, passed, failed, notGraded, cost (null when
|
|
9216
|
-
any run's cost is unknown),
|
|
9323
|
+
any run's cost is unknown), rewardPoints,
|
|
9324
|
+
rewardPointsMax, tokens (the total, or null),
|
|
9325
|
+
uploaded, uploadFailed
|
|
9217
9326
|
<test>/<n>/run.json One run: status and reason (from the checks), harness,
|
|
9218
9327
|
model (asked for, or null) and reportedModel (what
|
|
9219
9328
|
the harness reported running, or null), harnessArgs
|
|
@@ -9228,7 +9337,11 @@ Output: .firegoat/runs/<runId>/
|
|
|
9228
9337
|
own file, else <test>/<n>/session.jsonl made from the
|
|
9229
9338
|
stream) and sessionId (its id, the trace's), stderr,
|
|
9230
9339
|
checksLog, cleanupLog; turns and cost (null when the
|
|
9231
|
-
harness reports none),
|
|
9340
|
+
harness reports none), rewardPoints (earned),
|
|
9341
|
+
rewardPointsMax (the test's reward_points), tokens
|
|
9342
|
+
{input, output, cacheRead, cacheWrite, total} (null
|
|
9343
|
+
parts when the harness reports none), durationMs,
|
|
9344
|
+
startedAt; upload ({status: "uploaded",
|
|
9232
9345
|
url} | {status: "failed", error} | {status:
|
|
9233
9346
|
"skipped", reason}, null when not uploading)
|
|
9234
9347
|
<test>/<n>/transcript.txt What the agent did, below
|
|
@@ -9406,6 +9519,46 @@ async function stopLeftovers(opts) {
|
|
|
9406
9519
|
return leftover;
|
|
9407
9520
|
}
|
|
9408
9521
|
|
|
9522
|
+
// src/reward.ts
|
|
9523
|
+
function earned(status, rewardPointsMax) {
|
|
9524
|
+
return status === "pass" ? rewardPointsMax : 0;
|
|
9525
|
+
}
|
|
9526
|
+
function sum(values) {
|
|
9527
|
+
let n = 0;
|
|
9528
|
+
for (const v of values) {
|
|
9529
|
+
if (v === null)
|
|
9530
|
+
return null;
|
|
9531
|
+
n += v;
|
|
9532
|
+
}
|
|
9533
|
+
return n;
|
|
9534
|
+
}
|
|
9535
|
+
function tally(runs) {
|
|
9536
|
+
const rewardPoints = runs.reduce((s, r) => s + r.rewardPoints, 0);
|
|
9537
|
+
const t = tokens({
|
|
9538
|
+
input: sum(runs.map((r) => r.tokens.input)),
|
|
9539
|
+
output: sum(runs.map((r) => r.tokens.output)),
|
|
9540
|
+
cacheRead: sum(runs.map((r) => r.tokens.cacheRead)),
|
|
9541
|
+
cacheWrite: sum(runs.map((r) => r.tokens.cacheWrite))
|
|
9542
|
+
});
|
|
9543
|
+
return {
|
|
9544
|
+
rewardPoints,
|
|
9545
|
+
rewardPointsMax: runs.reduce((s, r) => s + r.rewardPointsMax, 0),
|
|
9546
|
+
tokens: t,
|
|
9547
|
+
tokensPerRewardPoint: rewardPoints === 0 || t.total === null ? null : Math.round(t.total / rewardPoints)
|
|
9548
|
+
};
|
|
9549
|
+
}
|
|
9550
|
+
function byGroup(runs) {
|
|
9551
|
+
const groups = [];
|
|
9552
|
+
for (const r of runs) {
|
|
9553
|
+
const g = groups.find((x) => x.harness === r.harness && x.model === r.model);
|
|
9554
|
+
if (g)
|
|
9555
|
+
g.runs.push(r);
|
|
9556
|
+
else
|
|
9557
|
+
groups.push({ harness: r.harness, model: r.model, runs: [r] });
|
|
9558
|
+
}
|
|
9559
|
+
return groups;
|
|
9560
|
+
}
|
|
9561
|
+
|
|
9409
9562
|
// src/session.ts
|
|
9410
9563
|
import { randomUUID } from "node:crypto";
|
|
9411
9564
|
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -10221,16 +10374,56 @@ function ramp(r) {
|
|
|
10221
10374
|
const end = r.state === "pass" ? "▲" : r.state === "fail" ? "╳" : r.state === "error" ? "╌" : r.state === "running" ? "●" : "";
|
|
10222
10375
|
return (blocks + end).padEnd(8);
|
|
10223
10376
|
}
|
|
10377
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
10378
|
+
function compactTokens(n) {
|
|
10379
|
+
const unit = (v, suffix) => `${v < 9.95 ? (Math.round(v * 10) / 10).toString() : Math.round(v)}${suffix}`;
|
|
10380
|
+
if (n < 1000)
|
|
10381
|
+
return String(n);
|
|
10382
|
+
if (n < 999500)
|
|
10383
|
+
return unit(n / 1000, "k");
|
|
10384
|
+
return unit(n / 1e6, "M");
|
|
10385
|
+
}
|
|
10224
10386
|
function usageParts(r) {
|
|
10225
10387
|
const parts = [];
|
|
10226
10388
|
if (typeof r.cost === "number")
|
|
10227
10389
|
parts.push(`$${r.cost.toFixed(2)}`);
|
|
10228
10390
|
if (typeof r.reportedTurns === "number")
|
|
10229
|
-
parts.push(
|
|
10391
|
+
parts.push(plural(r.reportedTurns, "turn"));
|
|
10392
|
+
if (r.tokens.total !== null)
|
|
10393
|
+
parts.push(`${compactTokens(r.tokens.total)} tokens`);
|
|
10230
10394
|
if (r.seconds !== undefined)
|
|
10231
10395
|
parts.push(`${r.seconds}s`);
|
|
10232
10396
|
return parts;
|
|
10233
10397
|
}
|
|
10398
|
+
function rewardLine(t, rows) {
|
|
10399
|
+
const points = `Reward points ${t.rewardPoints}/${t.rewardPointsMax}`;
|
|
10400
|
+
const unknown = rows.filter((r) => r.tokens.total === null).length;
|
|
10401
|
+
if (unknown > 0)
|
|
10402
|
+
return `${points} · tokens unknown for ${plural(unknown, "run")}`;
|
|
10403
|
+
if (t.tokensPerRewardPoint === null)
|
|
10404
|
+
return `${points} · no points, so no tokens per point`;
|
|
10405
|
+
return `${points} · ${compactTokens(t.tokensPerRewardPoint)} tokens per point`;
|
|
10406
|
+
}
|
|
10407
|
+
function comparison(rows) {
|
|
10408
|
+
const groups = byGroup(rows.map((r) => ({ ...r, harness: r.harnessName })));
|
|
10409
|
+
const label = (g) => `${g.harness} ${g.model ?? "(default model)"}`;
|
|
10410
|
+
const width = Math.min(40, Math.max(5, ...groups.map((g) => label(g).length)));
|
|
10411
|
+
const line = (cells) => ` ${cells[0].padEnd(width)} ${cells[1].padEnd(8)} ${cells[2].padEnd(10)} ${cells[3].padEnd(6)} ${cells[4]}`;
|
|
10412
|
+
return [
|
|
10413
|
+
line(["Group", "Points", "Tokens/pt", "Turns", "Time"]),
|
|
10414
|
+
...groups.map((g) => {
|
|
10415
|
+
const t = tally(g.runs);
|
|
10416
|
+
const turns = totalTurns(g.runs);
|
|
10417
|
+
return line([
|
|
10418
|
+
cut(label(g), width),
|
|
10419
|
+
`${t.rewardPoints}/${t.rewardPointsMax}`,
|
|
10420
|
+
g.runs.some((r) => r.tokens.total === null) ? "unknown" : t.tokensPerRewardPoint === null ? "no points" : compactTokens(t.tokensPerRewardPoint),
|
|
10421
|
+
turns === null ? "unknown" : String(turns),
|
|
10422
|
+
duration(g.runs.reduce((s, r) => s + (r.seconds ?? 0), 0))
|
|
10423
|
+
]);
|
|
10424
|
+
})
|
|
10425
|
+
];
|
|
10426
|
+
}
|
|
10234
10427
|
function usage(r) {
|
|
10235
10428
|
return usageParts(r).join(" ");
|
|
10236
10429
|
}
|
|
@@ -10313,17 +10506,9 @@ function plainUI(write) {
|
|
|
10313
10506
|
}
|
|
10314
10507
|
};
|
|
10315
10508
|
}
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
let build = null;
|
|
10320
|
-
let authText = "";
|
|
10321
|
-
const notes = [];
|
|
10322
|
-
let frame = 0;
|
|
10323
|
-
let drawn = 0;
|
|
10324
|
-
let finished = null;
|
|
10325
|
-
const width = () => Math.min(out.columns || 80, 100);
|
|
10326
|
-
const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
10509
|
+
var cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
10510
|
+
function timeline(t, p, w) {
|
|
10511
|
+
const { rows, build, finished, frame } = t;
|
|
10327
10512
|
const statusWord = (r) => {
|
|
10328
10513
|
switch (r.state) {
|
|
10329
10514
|
case "pass":
|
|
@@ -10352,77 +10537,113 @@ function ttyUI(out, opts) {
|
|
|
10352
10537
|
return p(C.gray, "○");
|
|
10353
10538
|
}
|
|
10354
10539
|
};
|
|
10355
|
-
const
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
10412
|
-
return out;
|
|
10413
|
-
}
|
|
10414
|
-
const tr = tier(rows);
|
|
10415
|
-
const goated = c.pass === rows.length && rows.length > 0;
|
|
10416
|
-
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10417
|
-
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
10418
|
-
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10419
|
-
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10420
|
-
const turns = totalTurns(rows);
|
|
10421
|
-
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
10422
|
-
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10423
|
-
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10540
|
+
const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
|
|
10541
|
+
const c = counts(rows);
|
|
10542
|
+
const doneCount = c.pass + c.fail + c.error;
|
|
10543
|
+
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
10544
|
+
const out = [
|
|
10545
|
+
...logoHeader(p, `firegoat ${t.version} eval · ${t.project}`, p(finished ? C.green : C.ember, badge), w),
|
|
10546
|
+
rule
|
|
10547
|
+
];
|
|
10548
|
+
if (t.authText)
|
|
10549
|
+
out.push(p(C.gray, ` Auth ${t.authText}`));
|
|
10550
|
+
for (const n of t.notes)
|
|
10551
|
+
out.push(p(C.gray, ` ${cut(n, w - 2)}`));
|
|
10552
|
+
if (build) {
|
|
10553
|
+
const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
10554
|
+
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` : ""}`);
|
|
10555
|
+
}
|
|
10556
|
+
const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
|
|
10557
|
+
const harnessCols = column((r) => r.harness?.length ?? 0);
|
|
10558
|
+
const modelCols = column((r) => r.model?.length ?? 0);
|
|
10559
|
+
const detailCols = w - 1 - (47 + (harnessCols > 0 ? harnessCols + 1 : 0) + (modelCols > 0 ? modelCols + 1 : 0));
|
|
10560
|
+
rows.forEach((r, i) => {
|
|
10561
|
+
out.push(p(C.gray, " │"));
|
|
10562
|
+
const num = String(i + 1).padStart(2, "0");
|
|
10563
|
+
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
10564
|
+
const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
|
|
10565
|
+
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
10566
|
+
let detail = "";
|
|
10567
|
+
if (r.state === "running")
|
|
10568
|
+
detail = p(C.gray, `Turn ${r.turns}`);
|
|
10569
|
+
else if (r.state === "pass" || r.state === "fail")
|
|
10570
|
+
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, cut(usage(r), Math.max(1, detailCols - 12)))}`;
|
|
10571
|
+
else if (r.state === "error")
|
|
10572
|
+
detail = p(C.gray, cut(usage(r), Math.max(1, detailCols)));
|
|
10573
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
10574
|
+
if (r.state === "running" && r.command)
|
|
10575
|
+
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
10576
|
+
if (r.state === "fail")
|
|
10577
|
+
for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
|
|
10578
|
+
out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
|
|
10579
|
+
out.push(p(C.gray, ` │ ${cut(ck.error ?? `exit ${ck.code}`, w - 14)}`));
|
|
10580
|
+
}
|
|
10581
|
+
if (r.state === "error")
|
|
10582
|
+
out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
|
|
10583
|
+
if (r.upload === "uploading")
|
|
10584
|
+
out.push(p(C.gray, " │ ↑ Uploading…"));
|
|
10585
|
+
else if (r.upload?.status === "uploaded")
|
|
10586
|
+
out.push(` ${p(C.gray, "│")} ${p(C.cyan, `↑ ${r.upload.url}`)}`);
|
|
10587
|
+
else if (r.upload)
|
|
10588
|
+
out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
|
|
10589
|
+
});
|
|
10590
|
+
out.push(rule);
|
|
10591
|
+
if (!finished) {
|
|
10592
|
+
const track = Math.max(10, w - 34);
|
|
10593
|
+
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
10594
|
+
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
10595
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
10424
10596
|
return out;
|
|
10425
|
-
}
|
|
10597
|
+
}
|
|
10598
|
+
const tr = tier(rows);
|
|
10599
|
+
const goated = c.pass === rows.length && rows.length > 0;
|
|
10600
|
+
const passFail = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10601
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : passFail;
|
|
10602
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10603
|
+
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10604
|
+
const turns = totalTurns(rows);
|
|
10605
|
+
const reward = tally(rows);
|
|
10606
|
+
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${plural(turns, "turn")}`}${reward.tokens.total === null ? "" : ` · ${compactTokens(reward.tokens.total)} tokens`} · ${duration(finished.seconds)}`));
|
|
10607
|
+
out.push(` ${rewardLine(reward, rows)}`);
|
|
10608
|
+
if (byGroup(rows.map((r) => ({ ...r, harness: r.harnessName }))).length > 1)
|
|
10609
|
+
out.push(...comparison(rows).map((l) => p(C.gray, l)));
|
|
10610
|
+
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10611
|
+
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10612
|
+
return out;
|
|
10613
|
+
}
|
|
10614
|
+
var RESULTS_WIDTH = 100;
|
|
10615
|
+
function resultsBlock(t) {
|
|
10616
|
+
return timeline({
|
|
10617
|
+
project: t.project,
|
|
10618
|
+
version: t.version,
|
|
10619
|
+
rows: t.rows,
|
|
10620
|
+
build: null,
|
|
10621
|
+
authText: "",
|
|
10622
|
+
notes: [],
|
|
10623
|
+
finished: { runId: t.runId, runsDir: t.runsDir, seconds: t.seconds },
|
|
10624
|
+
frame: 0
|
|
10625
|
+
}, painter(false), RESULTS_WIDTH);
|
|
10626
|
+
}
|
|
10627
|
+
function ttyUI(out, opts) {
|
|
10628
|
+
const p = painter(opts.color);
|
|
10629
|
+
let rows = [];
|
|
10630
|
+
let build = null;
|
|
10631
|
+
let authText = "";
|
|
10632
|
+
const notes = [];
|
|
10633
|
+
let frame = 0;
|
|
10634
|
+
let drawn = 0;
|
|
10635
|
+
let finished = null;
|
|
10636
|
+
const width = () => Math.min(out.columns || 80, 100);
|
|
10637
|
+
const lines = () => timeline({
|
|
10638
|
+
project: opts.project,
|
|
10639
|
+
version: opts.version,
|
|
10640
|
+
rows,
|
|
10641
|
+
build,
|
|
10642
|
+
authText,
|
|
10643
|
+
notes,
|
|
10644
|
+
finished,
|
|
10645
|
+
frame
|
|
10646
|
+
}, p, width());
|
|
10426
10647
|
const rowsOf = (line) => {
|
|
10427
10648
|
const visible = line.replace(ANSI, "");
|
|
10428
10649
|
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
@@ -10873,6 +11094,9 @@ async function runOne(opts) {
|
|
|
10873
11094
|
leftover: [],
|
|
10874
11095
|
turns: null,
|
|
10875
11096
|
cost: null,
|
|
11097
|
+
rewardPoints: 0,
|
|
11098
|
+
rewardPointsMax: test.rewardPoints,
|
|
11099
|
+
tokens: tokens(),
|
|
10876
11100
|
durationMs: 0,
|
|
10877
11101
|
startedAt: new Date(started).toISOString(),
|
|
10878
11102
|
upload: null
|
|
@@ -10904,6 +11128,7 @@ async function runOne(opts) {
|
|
|
10904
11128
|
r.leftover = await stopLeftovers({ group: agent.group, workdir });
|
|
10905
11129
|
r.status = status;
|
|
10906
11130
|
r.reason = reason;
|
|
11131
|
+
r.rewardPoints = earned(status, r.rewardPointsMax);
|
|
10907
11132
|
r.durationMs = Date.now() - started;
|
|
10908
11133
|
writeRunJson(r);
|
|
10909
11134
|
return r;
|
|
@@ -10994,6 +11219,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
|
10994
11219
|
});
|
|
10995
11220
|
r.turns = outcome.turns;
|
|
10996
11221
|
r.cost = outcome.cost;
|
|
11222
|
+
r.tokens = outcome.tokens;
|
|
10997
11223
|
if (!outcome.ok)
|
|
10998
11224
|
return ["error", `not graded: ${outcome.reason}`];
|
|
10999
11225
|
if (!r.session)
|
|
@@ -11043,6 +11269,16 @@ function uploadedSummary(origin, results, runDir) {
|
|
|
11043
11269
|
for (const u of urls)
|
|
11044
11270
|
console.log(` ${u}`);
|
|
11045
11271
|
}
|
|
11272
|
+
var RESULTS_EDGE = "═".repeat(RESULTS_WIDTH);
|
|
11273
|
+
function printResults(block, file) {
|
|
11274
|
+
console.log("");
|
|
11275
|
+
console.log("Results (paste the block between the ═ lines as-is, in a code block):");
|
|
11276
|
+
console.log(RESULTS_EDGE);
|
|
11277
|
+
for (const line of block)
|
|
11278
|
+
console.log(line);
|
|
11279
|
+
console.log(RESULTS_EDGE);
|
|
11280
|
+
console.log(`Results block saved to ${file}`);
|
|
11281
|
+
}
|
|
11046
11282
|
async function evalCommand(args, root) {
|
|
11047
11283
|
if (args.includes("--help") || args.includes("-h")) {
|
|
11048
11284
|
console.log(EVAL_HELP);
|
|
@@ -11206,8 +11442,12 @@ async function evalCommand(args, root) {
|
|
|
11206
11442
|
const rows = plan.map((p) => ({
|
|
11207
11443
|
...p,
|
|
11208
11444
|
harness: showHarness ? p.harness : null,
|
|
11445
|
+
harnessName: p.harness,
|
|
11209
11446
|
state: "queued",
|
|
11210
|
-
turns: 0
|
|
11447
|
+
turns: 0,
|
|
11448
|
+
rewardPoints: 0,
|
|
11449
|
+
rewardPointsMax: tests.find((t) => t.name === p.test).rewardPoints,
|
|
11450
|
+
tokens: tokens()
|
|
11211
11451
|
}));
|
|
11212
11452
|
if (config.build.length > 0) {
|
|
11213
11453
|
const buildStarted = Date.now();
|
|
@@ -11265,6 +11505,8 @@ async function evalCommand(args, root) {
|
|
|
11265
11505
|
reason: r.reason,
|
|
11266
11506
|
checks: r.checks,
|
|
11267
11507
|
cost: r.cost,
|
|
11508
|
+
rewardPoints: r.rewardPoints,
|
|
11509
|
+
tokens: r.tokens,
|
|
11268
11510
|
seconds: Math.round(r.durationMs / 1000),
|
|
11269
11511
|
command: undefined
|
|
11270
11512
|
});
|
|
@@ -11302,6 +11544,7 @@ async function evalCommand(args, root) {
|
|
|
11302
11544
|
fail: EXIT.fail,
|
|
11303
11545
|
pass: EXIT.pass
|
|
11304
11546
|
}[status];
|
|
11547
|
+
const reward = tally(results);
|
|
11305
11548
|
const doc = {
|
|
11306
11549
|
firegoatVersion: package_default.version,
|
|
11307
11550
|
runId,
|
|
@@ -11311,6 +11554,15 @@ async function evalCommand(args, root) {
|
|
|
11311
11554
|
harnesses: harnessNames,
|
|
11312
11555
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
11313
11556
|
runs: results,
|
|
11557
|
+
...reward,
|
|
11558
|
+
groups: byGroup(results).map((g) => ({
|
|
11559
|
+
harness: g.harness,
|
|
11560
|
+
model: g.model,
|
|
11561
|
+
runs: g.runs.length,
|
|
11562
|
+
...tally(g.runs),
|
|
11563
|
+
turns: g.runs.every((r) => r.turns !== null) ? g.runs.reduce((s, r) => s + r.turns, 0) : null,
|
|
11564
|
+
durationMs: g.runs.reduce((s, r) => s + r.durationMs, 0)
|
|
11565
|
+
})),
|
|
11314
11566
|
totals,
|
|
11315
11567
|
tier: tier(rows),
|
|
11316
11568
|
status,
|
|
@@ -11329,16 +11581,15 @@ async function evalCommand(args, root) {
|
|
|
11329
11581
|
failed: totals.fail,
|
|
11330
11582
|
notGraded: totals.error,
|
|
11331
11583
|
cost: results.every((r) => r.cost !== null) ? results.reduce((sum, r) => sum + r.cost, 0) : null,
|
|
11584
|
+
rewardPoints: reward.rewardPoints,
|
|
11585
|
+
rewardPointsMax: reward.rewardPointsMax,
|
|
11586
|
+
tokens: reward.tokens.total,
|
|
11332
11587
|
uploaded,
|
|
11333
11588
|
uploadFailed: uploadsFailed
|
|
11334
11589
|
})}
|
|
11335
11590
|
`);
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
runsDir: runDir,
|
|
11339
|
-
rows,
|
|
11340
|
-
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
11341
|
-
});
|
|
11591
|
+
const seconds = Math.round((Date.now() - evalStarted) / 1000);
|
|
11592
|
+
ui.done({ runId, runsDir: runDir, rows, seconds });
|
|
11342
11593
|
if (upload && !opts.json)
|
|
11343
11594
|
uploadedSummary(upload.origin, results, runDir);
|
|
11344
11595
|
for (const r of results)
|
|
@@ -11350,6 +11601,20 @@ async function evalCommand(args, root) {
|
|
|
11350
11601
|
console.error(`firegoat eval: Warning: ${r.test}#${r.n} left ${r.leftover.length} process${r.leftover.length === 1 ? "" : "es"} running, stopped after cleanup: ${r.leftover.map((l) => `${l.pid} ${l.command}`).join("; ")} (run.json leftover)`);
|
|
11351
11602
|
if (uploadsFailed > 0)
|
|
11352
11603
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
11604
|
+
const block = resultsBlock({
|
|
11605
|
+
project: basename2(root),
|
|
11606
|
+
version: package_default.version,
|
|
11607
|
+
rows,
|
|
11608
|
+
runId,
|
|
11609
|
+
runsDir: runDir,
|
|
11610
|
+
seconds
|
|
11611
|
+
});
|
|
11612
|
+
const resultsFile = join10(runDir, "results.txt");
|
|
11613
|
+
writeFileSync5(resultsFile, `${block.join(`
|
|
11614
|
+
`)}
|
|
11615
|
+
`);
|
|
11616
|
+
if (!opts.json && !stream.isTTY)
|
|
11617
|
+
printResults(block, resultsFile);
|
|
11353
11618
|
if (opts.json)
|
|
11354
11619
|
console.log(JSON.stringify(doc, null, 2));
|
|
11355
11620
|
if (status === "error")
|
|
@@ -12033,6 +12298,9 @@ tests:
|
|
|
12033
12298
|
grep -Eiq '^agent: .*(you.*goat|goat.*you)' "$FIREGOAT_TRANSCRIPT"
|
|
12034
12299
|
# Seconds before the agent run is stopped.
|
|
12035
12300
|
timeout: 600
|
|
12301
|
+
# What a run earns when its checks pass (default 1): give harder end goals
|
|
12302
|
+
# more. eval prints tokens per point earned, the value per token.
|
|
12303
|
+
# reward_points: 3
|
|
12036
12304
|
`;
|
|
12037
12305
|
}
|
|
12038
12306
|
var AGENT_PROMPT = "Scan my project and setup 2-3 agent experience prompts to test general usage of the app. Use firegoat skills";
|
|
@@ -12049,7 +12317,7 @@ function copyToClipboard(text) {
|
|
|
12049
12317
|
});
|
|
12050
12318
|
}
|
|
12051
12319
|
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
12052
|
-
function
|
|
12320
|
+
function cut2(s, n) {
|
|
12053
12321
|
return s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}~`;
|
|
12054
12322
|
}
|
|
12055
12323
|
|
|
@@ -12064,8 +12332,8 @@ class Timeline {
|
|
|
12064
12332
|
line(mark, n, label, status = "") {
|
|
12065
12333
|
const head = ` ${mark} ${this.paint("90", String(n).padStart(2, "0"))} `;
|
|
12066
12334
|
if (!status)
|
|
12067
|
-
return head +
|
|
12068
|
-
return `${head}${label.padEnd(28)}${this.paint("90",
|
|
12335
|
+
return head + cut2(label, this.width - 9);
|
|
12336
|
+
return `${head}${label.padEnd(28)}${this.paint("90", cut2(status, this.width - 37))}`;
|
|
12069
12337
|
}
|
|
12070
12338
|
done(n, label, status) {
|
|
12071
12339
|
return this.line(this.paint("32", "●"), n, label, status);
|
|
@@ -12088,7 +12356,7 @@ class Timeline {
|
|
|
12088
12356
|
return select(block, options.length, (current) => [
|
|
12089
12357
|
this.line(this.paint("38;5;208", "◉"), n, question),
|
|
12090
12358
|
...options.map(([label, note], i) => {
|
|
12091
|
-
const text =
|
|
12359
|
+
const text = cut2(`${label.padEnd(cols)}${note}`, room);
|
|
12092
12360
|
return i === current ? `${Timeline.INDENT}${this.paint("38;5;208", `● ${text}`)}` : this.paint("90", `${Timeline.INDENT}○ ${text}`);
|
|
12093
12361
|
}),
|
|
12094
12362
|
this.paint("90", `${Timeline.INDENT}Up/down to move, enter to pick`)
|