firegoat 0.20.0 → 0.21.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 +18 -10
- package/dist/cli.js +93 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npx firegoat@latest init # Creates firegoat.yaml and .firegoat/, installs th
|
|
|
12
12
|
npx firegoat@latest eval # Runs your claude on each test, in .firegoat/runs/<runId>/<test>/<n>/work
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
That's it: two commands. Use `npx firegoat@latest`: plain `npx firegoat` can keep running an old cached version. `eval` prints its version first (`firegoat
|
|
15
|
+
That's it: two commands. Use `npx firegoat@latest`: plain `npx firegoat` can keep running an old cached version. `eval` prints its version first (`firegoat X.Y.Z`), and `summary.json` records it as `firegoatVersion`. `claude` must be installed and logged in (run `claude` once).
|
|
16
16
|
|
|
17
17
|
`init` installs the firegoat-onboarding skill globally, into `~/.claude/skills/` (in a terminal it asks: global, this repo, or none). `--local-skills` puts it in this repo's `.claude/skills/` instead (commit it so teammates and CI get it); `--no-skills` skips it.
|
|
18
18
|
|
|
@@ -160,16 +160,24 @@ tests:
|
|
|
160
160
|
|
|
161
161
|
### Testing an MCP server
|
|
162
162
|
|
|
163
|
+
Commit the server's config as `fixtures/mcp.json`:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{"mcpServers": {"acme": {"command": "./node_modules/.bin/acme-mcp",
|
|
167
|
+
"env": {"HOME": "${FIREGOAT_HOME}"}}}}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Copy it in `build:` and pass it with `claude_args:`:
|
|
171
|
+
|
|
163
172
|
```yaml
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
claude_args: [--mcp-config, .mcp.json]
|
|
173
|
+
build:
|
|
174
|
+
- npm pack --pack-destination "$FIREGOAT_OUT"
|
|
175
|
+
- cp -R fixtures/. "$FIREGOAT_OUT"
|
|
176
|
+
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
177
|
+
claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
170
178
|
```
|
|
171
179
|
|
|
172
|
-
|
|
180
|
+
claude gets `FIREGOAT_HOME`, `FIREGOAT_WORKDIR` and `FIREGOAT_OUT` in its own env, set per run, and expands `${VAR}` in an MCP config's `command`, `args`, `env`, `url` and `headers` from its env (an unset var stays the literal text). Relative paths in the config are from the run's folder, claude's cwd: `./node_modules/.bin/acme-mcp` is the build under test, installed by `install:`. MCP servers that claude starts inherit claude's own env, which keeps your real `HOME` (claude needs it for its login): set `"env": {"HOME": "${FIREGOAT_HOME}"}` on the server, as above, or it writes to your real home. Per-run state the server keeps elsewhere goes the same way, e.g. `"ACME_DB": "${FIREGOAT_WORKDIR}/acme.db"`. `--strict-mcp-config` is always on, so only the configs you pass load. To check that the agent used a tool, grep the transcript: `grep -Eq '^\[[0-9]+\] mcp__acme__search' "$FIREGOAT_TRANSCRIPT"`.
|
|
173
181
|
|
|
174
182
|
With `--chrome`, claude points the Claude in Chrome extension's native host (`~/Library/Application Support/<browser>/NativeMessagingHosts/com.anthropic.claude_code_browser_extension.json`) at `.firegoat/.claude`. If you delete `.firegoat/`, run a normal `claude --chrome` once to point it back.
|
|
175
183
|
|
|
@@ -211,9 +219,9 @@ Limits:
|
|
|
211
219
|
## What eval does
|
|
212
220
|
|
|
213
221
|
- claude runs on its own login (or `ANTHROPIC_API_KEY`, if set) with `CLAUDE_CONFIG_DIR=.firegoat/.claude` and `--setting-sources "" --strict-mcp-config --disable-slash-commands`: none of your settings, CLAUDE.md, plugins, skills or MCP servers (add flags with `claude_args:`, see above). claude saves its session there, in its own format.
|
|
214
|
-
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. `.firegoat/` holds local run output and ignores itself in git. Keep test fixtures (scripts, sample data)
|
|
222
|
+
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. `.firegoat/` holds local run output and ignores itself in git. Keep test fixtures (scripts, sample data) in `fixtures/` next to `firegoat.yaml`, committed.
|
|
215
223
|
- In a linked git worktree (`git worktree add`), `.firegoat/runs/` and `.firegoat/.claude/` are in the main checkout, so runs (and their session files) survive `git worktree remove`; `firegoat.yaml`, fixtures and `build:` stay in the worktree. Each `summary.json` records `dir` (where eval ran) and `git: {branch, commit, dirty}`, and `.firegoat/runs/index.jsonl` gets one line per eval (`runId, startedAt, dir, branch, commit, passed, failed, notGraded, cost, uploaded, uploadFailed`) to tell runs from different branches apart.
|
|
216
|
-
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, build, install, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it. Every `FIREGOAT_*` var firegoat was started with (`FIREGOAT_API_KEY` above all) is removed too, and so are the vars a parent Claude Code session sets (when firegoat runs from claude, or its agent runs firegoat), so the claude under test doesn't act as a nested 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`. Login and provider vars stay: `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`, `CLAUDE_CODE_USE_*` (Bedrock, Vertex, ...), `CLAUDE_CODE_SKIP_*_AUTH`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `CLAUDE_CODE_CLIENT_KEY_PASSPHRASE`.
|
|
224
|
+
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, build, install, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it. Every `FIREGOAT_*` var firegoat was started with (`FIREGOAT_API_KEY` above all) is removed too (claude then gets the run's own `FIREGOAT_OUT`, `FIREGOAT_WORKDIR` and `FIREGOAT_HOME`, for MCP configs; the agent's commands see them too), and so are the vars a parent Claude Code session sets (when firegoat runs from claude, or its agent runs firegoat), so the claude under test doesn't act as a nested 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`. Login and provider vars stay: `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`, `CLAUDE_CODE_USE_*` (Bedrock, Vertex, ...), `CLAUDE_CODE_SKIP_*_AUTH`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `CLAUDE_CODE_CLIENT_KEY_PASSPHRASE`.
|
|
217
225
|
- Runs (tests × models × `runs`) go 4 at a time, each in its own folder with its own session and HOME; `--parallel N` changes that, `--parallel 1` runs them one after another. `summary.json` lists them in `firegoat.yaml` order.
|
|
218
226
|
- Script checks run in the agent's folder afterwards and set the exit code. `end_goal` is not graded by firegoat: it's for you, or your coding agent, to judge from `run.json`, the session and the folder.
|
|
219
227
|
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `transcript.txt` (each tool call with its command and short output, the final answer, files made; checks get it as `$FIREGOAT_TRANSCRIPT`; greppable line by line: every line of a shell call starts with `[n] $ `, every line of the agent's text and final answer with `agent: `, output is indented, so `grep -E '^\[[0-9]+\] \$ .*npm install'` matches only commands, also inside heredocs and multi-line scripts), `run.json` (status, prompt, end_goal, check results, paths), `work/`, `home/` and `home.env` (the run's HOME), logs. Read the transcript to judge `end_goal`; claude's full session file is ~50x larger. `summary.json` covers the whole eval; `--json` prints it.
|
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.21.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7448,8 +7448,15 @@ async function runClaude(opts) {
|
|
|
7448
7448
|
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7449
7449
|
};
|
|
7450
7450
|
}
|
|
7451
|
-
const sessionId = String(init.session_id);
|
|
7452
7451
|
const claudeModel = typeof init.model === "string" ? init.model : null;
|
|
7452
|
+
if (typeof init.session_id !== "string" || init.session_id === "")
|
|
7453
|
+
return {
|
|
7454
|
+
events,
|
|
7455
|
+
claudeModel,
|
|
7456
|
+
ok: false,
|
|
7457
|
+
reason: "claude's init line has no session_id"
|
|
7458
|
+
};
|
|
7459
|
+
const sessionId = init.session_id;
|
|
7453
7460
|
const result = events.findLast((e) => e?.type === "result");
|
|
7454
7461
|
if (exited.timedOut)
|
|
7455
7462
|
return {
|
|
@@ -7646,7 +7653,7 @@ async function send(opts) {
|
|
|
7646
7653
|
function replyError(step, r) {
|
|
7647
7654
|
const j = r.json ?? {};
|
|
7648
7655
|
const error = typeof j.error === "string" ? j.error : "no error message";
|
|
7649
|
-
const more = j.error_description ?? j.details
|
|
7656
|
+
const more = j.error_description ?? j.details;
|
|
7650
7657
|
const details = more === undefined ? "" : ` ${typeof more === "string" ? more : JSON.stringify(more)}`;
|
|
7651
7658
|
return new ApiError(printable(`${step} ${r.status}: ${error}${details}`), r.status);
|
|
7652
7659
|
}
|
|
@@ -7816,7 +7823,12 @@ var GITIGNORE = `*
|
|
|
7816
7823
|
function ensureFiregoatDir(root) {
|
|
7817
7824
|
const dir = join2(root, ".firegoat");
|
|
7818
7825
|
mkdirSync2(dir, { recursive: true });
|
|
7819
|
-
|
|
7826
|
+
try {
|
|
7827
|
+
writeFileSync(join2(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
|
|
7828
|
+
} catch (e) {
|
|
7829
|
+
if (e.code !== "EEXIST")
|
|
7830
|
+
throw e;
|
|
7831
|
+
}
|
|
7820
7832
|
return dir;
|
|
7821
7833
|
}
|
|
7822
7834
|
|
|
@@ -7918,8 +7930,8 @@ firegoat-onboarding skill globally, into ~/.claude/skills/ (npx skills add
|
|
|
7918
7930
|
firegoat-ai/skills -g; in a terminal it asks: global, this repo, or none), offers
|
|
7919
7931
|
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
7920
7932
|
paste into your coding agent. .firegoat/ holds local run output and ignores itself
|
|
7921
|
-
in git. Keep test fixtures (helper scripts, sample data)
|
|
7922
|
-
|
|
7933
|
+
in git. Keep test fixtures (helper scripts, sample data) in fixtures/ next to
|
|
7934
|
+
firegoat.yaml, committed.
|
|
7923
7935
|
|
|
7924
7936
|
--force Replace an existing firegoat.yaml
|
|
7925
7937
|
--local-skills Install the skill into this repo's .claude/skills/ instead
|
|
@@ -8008,19 +8020,23 @@ firegoat.yaml reference:
|
|
|
8008
8020
|
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
8009
8021
|
npm_command, INIT_CWD; e.g. npm_config_yes=true from npx -y) are removed from
|
|
8010
8022
|
every env firegoat starts, unless env: sets them. So is every FIREGOAT_* var
|
|
8011
|
-
(FIREGOAT_API_KEY above all) firegoat itself was started with.
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8023
|
+
(FIREGOAT_API_KEY above all) firegoat itself was started with. claude itself
|
|
8024
|
+
gets $FIREGOAT_OUT, $FIREGOAT_WORKDIR and $FIREGOAT_HOME (for MCP configs, below),
|
|
8025
|
+
so the agent's commands see them too.
|
|
8026
|
+
|
|
8027
|
+
Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
|
|
8028
|
+
and pass it with claude_args. Only the configs you pass load: --strict-mcp-config
|
|
8029
|
+
is always on. An MCP server inherits claude's env, which keeps your real HOME
|
|
8030
|
+
(claude's login is there), so set the server's HOME to the run's home, or it
|
|
8031
|
+
writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
|
|
8032
|
+
in its env and expands \${VAR} in the config (an unset one stays literal);
|
|
8033
|
+
relative paths in it are from the run's folder, claude's cwd:
|
|
8034
|
+
fixtures/mcp.json:
|
|
8035
|
+
{"mcpServers": {"acme": {"command": "./node_modules/.bin/acme-mcp",
|
|
8036
|
+
"env": {"HOME": "\${FIREGOAT_HOME}"}}}}
|
|
8037
|
+
firegoat.yaml:
|
|
8038
|
+
build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
8039
|
+
claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
8024
8040
|
|
|
8025
8041
|
Example:
|
|
8026
8042
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -8136,8 +8152,7 @@ fixtures and build: stay in the current folder.
|
|
|
8136
8152
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
8137
8153
|
stops early); progress goes to stderr
|
|
8138
8154
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
8139
|
-
--claude-bin PATH Path to claude (
|
|
8140
|
-
PATH)
|
|
8155
|
+
--claude-bin PATH Path to claude (default: claude on PATH)
|
|
8141
8156
|
|
|
8142
8157
|
Upload: after firegoat login (or with FIREGOAT_API_KEY), each run's claude session,
|
|
8143
8158
|
checks.txt and stderr.txt go to firegoat.ai as a private trace, and its URL is
|
|
@@ -8154,8 +8169,8 @@ Output: .firegoat/runs/<runId>/
|
|
|
8154
8169
|
git {branch, commit, dirty} (no git outside a repo)
|
|
8155
8170
|
and upload {origin, email} (null when not uploading)
|
|
8156
8171
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
8157
|
-
commit, passed, failed, notGraded, cost
|
|
8158
|
-
uploadFailed
|
|
8172
|
+
commit, passed, failed, notGraded, cost (null when
|
|
8173
|
+
any run's cost is unknown), uploaded, uploadFailed
|
|
8159
8174
|
<test>/<n>/run.json One run: status and reason (from the checks), model
|
|
8160
8175
|
(asked for, or null) and claudeModel (what claude's
|
|
8161
8176
|
init line reported), claudeArgs (claude_args
|
|
@@ -8306,6 +8321,13 @@ function describeCall(step, name, input) {
|
|
|
8306
8321
|
return `[${step}] ${name} ${input[key]}`;
|
|
8307
8322
|
return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, 300)}`;
|
|
8308
8323
|
}
|
|
8324
|
+
function describeToolUse(step, b) {
|
|
8325
|
+
if (typeof b.name !== "string" || b.name === "")
|
|
8326
|
+
return `[${step}] (tool call without a name)`;
|
|
8327
|
+
if (typeof b.input !== "object" || b.input === null)
|
|
8328
|
+
return `[${step}] ${b.name} (no input)`;
|
|
8329
|
+
return describeCall(step, b.name, b.input);
|
|
8330
|
+
}
|
|
8309
8331
|
function resultText(content) {
|
|
8310
8332
|
if (typeof content === "string")
|
|
8311
8333
|
return content;
|
|
@@ -8341,7 +8363,7 @@ function writeTranscript(opts) {
|
|
|
8341
8363
|
if (e?.type === "assistant")
|
|
8342
8364
|
for (const b of content) {
|
|
8343
8365
|
if (b.type === "tool_use")
|
|
8344
|
-
out.push(
|
|
8366
|
+
out.push(describeToolUse(++step, b));
|
|
8345
8367
|
else if (b.type === "text" && typeof b.text === "string") {
|
|
8346
8368
|
lastText = out.length;
|
|
8347
8369
|
out.push(prose(truncate(b.text, 30, TEXT_CHARS)));
|
|
@@ -8418,23 +8440,38 @@ function ramp(r) {
|
|
|
8418
8440
|
const end = r.state === "pass" ? "▲" : r.state === "fail" ? "╳" : r.state === "error" ? "╌" : r.state === "running" ? "●" : "";
|
|
8419
8441
|
return (blocks + end).padEnd(8);
|
|
8420
8442
|
}
|
|
8421
|
-
function
|
|
8443
|
+
function usageParts(r) {
|
|
8422
8444
|
const parts = [];
|
|
8423
|
-
if (
|
|
8445
|
+
if (typeof r.cost === "number")
|
|
8424
8446
|
parts.push(`$${r.cost.toFixed(2)}`);
|
|
8425
|
-
|
|
8447
|
+
if (typeof r.reportedTurns === "number")
|
|
8448
|
+
parts.push(`${r.reportedTurns} turns`);
|
|
8426
8449
|
if (r.seconds !== undefined)
|
|
8427
8450
|
parts.push(`${r.seconds}s`);
|
|
8428
|
-
return parts
|
|
8451
|
+
return parts;
|
|
8452
|
+
}
|
|
8453
|
+
function usage(r) {
|
|
8454
|
+
return usageParts(r).join(" ");
|
|
8429
8455
|
}
|
|
8430
8456
|
function checksPassed(r) {
|
|
8431
8457
|
const all = r.checks ?? [];
|
|
8432
8458
|
return `${all.filter((c) => c.code === 0).length}/${all.length} checks`;
|
|
8433
8459
|
}
|
|
8434
|
-
function
|
|
8435
|
-
const
|
|
8436
|
-
const
|
|
8437
|
-
|
|
8460
|
+
function totalCost(rows) {
|
|
8461
|
+
const done = rows.filter((r) => r.state !== "queued" && r.state !== "running");
|
|
8462
|
+
const unknown = done.filter((r) => typeof r.cost !== "number").length;
|
|
8463
|
+
if (unknown > 0)
|
|
8464
|
+
return `cost unknown for ${unknown} run${unknown === 1 ? "" : "s"}`;
|
|
8465
|
+
return `$${done.reduce((s, r) => s + r.cost, 0).toFixed(2)}`;
|
|
8466
|
+
}
|
|
8467
|
+
function totalTurns(rows) {
|
|
8468
|
+
let sum = 0;
|
|
8469
|
+
for (const r of rows) {
|
|
8470
|
+
if (typeof r.reportedTurns !== "number")
|
|
8471
|
+
return null;
|
|
8472
|
+
sum += r.reportedTurns;
|
|
8473
|
+
}
|
|
8474
|
+
return sum;
|
|
8438
8475
|
}
|
|
8439
8476
|
function duration(seconds) {
|
|
8440
8477
|
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
@@ -8476,9 +8513,9 @@ function plainUI(write) {
|
|
|
8476
8513
|
continue;
|
|
8477
8514
|
printed.add(r);
|
|
8478
8515
|
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
8479
|
-
const u =
|
|
8516
|
+
const u = usageParts(r).join(", ");
|
|
8480
8517
|
const model = r.model === null ? "" : ` [${r.model}]`;
|
|
8481
|
-
write(`${label} ${r.test}#${r.n}${model} ${r.reason} (${u})`);
|
|
8518
|
+
write(`${label} ${r.test}#${r.n}${model} ${r.reason}${u ? ` (${u})` : ""}`);
|
|
8482
8519
|
}
|
|
8483
8520
|
for (const r of rows) {
|
|
8484
8521
|
if (uploadPrinted.has(r) || !r.upload || r.upload === "uploading")
|
|
@@ -8489,10 +8526,9 @@ function plainUI(write) {
|
|
|
8489
8526
|
},
|
|
8490
8527
|
done: ({ runsDir, rows: all }) => {
|
|
8491
8528
|
const c = counts(all);
|
|
8492
|
-
const t = total(all);
|
|
8493
8529
|
const tr = tier(all);
|
|
8494
8530
|
write("");
|
|
8495
|
-
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs,
|
|
8531
|
+
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs, ${totalCost(all)})${tr ? `, tier ${tr}` : ""}`);
|
|
8496
8532
|
write(`Runs: ${runsDir} (end_goal is for you to judge: read <test>/<n>/transcript.txt)`);
|
|
8497
8533
|
}
|
|
8498
8534
|
};
|
|
@@ -8585,12 +8621,11 @@ function ttyUI(out, opts) {
|
|
|
8585
8621
|
out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
|
|
8586
8622
|
});
|
|
8587
8623
|
out.push(rule);
|
|
8588
|
-
const t = total(rows);
|
|
8589
8624
|
if (!finished) {
|
|
8590
8625
|
const track = Math.max(10, w - 34);
|
|
8591
8626
|
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
8592
8627
|
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
8593
|
-
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} ·
|
|
8628
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
8594
8629
|
return out;
|
|
8595
8630
|
}
|
|
8596
8631
|
const tr = tier(rows);
|
|
@@ -8599,7 +8634,8 @@ function ttyUI(out, opts) {
|
|
|
8599
8634
|
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
8600
8635
|
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
8601
8636
|
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
8602
|
-
|
|
8637
|
+
const turns = totalTurns(rows);
|
|
8638
|
+
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
8603
8639
|
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
8604
8640
|
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
8605
8641
|
return out;
|
|
@@ -8666,6 +8702,12 @@ function label(v, what) {
|
|
|
8666
8702
|
throw new Error(`${what} is ${v.length} characters, the upload takes at most ${MAX_LABEL}`);
|
|
8667
8703
|
return v;
|
|
8668
8704
|
}
|
|
8705
|
+
function projectName(v) {
|
|
8706
|
+
const name = label(v, "Project name");
|
|
8707
|
+
if (name === null)
|
|
8708
|
+
throw new Error("Project name is empty");
|
|
8709
|
+
return name;
|
|
8710
|
+
}
|
|
8669
8711
|
function runModel(r) {
|
|
8670
8712
|
return {
|
|
8671
8713
|
requested: label(r.model, "Model"),
|
|
@@ -8731,9 +8773,11 @@ async function uploadRun(t, r) {
|
|
|
8731
8773
|
};
|
|
8732
8774
|
bytes[name] = b;
|
|
8733
8775
|
}
|
|
8776
|
+
let project;
|
|
8734
8777
|
let model;
|
|
8735
8778
|
let git;
|
|
8736
8779
|
try {
|
|
8780
|
+
project = projectName(t.project);
|
|
8737
8781
|
model = runModel(r);
|
|
8738
8782
|
git = t.git ? runGit(t.git) : undefined;
|
|
8739
8783
|
} catch (e) {
|
|
@@ -8742,7 +8786,7 @@ async function uploadRun(t, r) {
|
|
|
8742
8786
|
const body = {
|
|
8743
8787
|
sessionId,
|
|
8744
8788
|
runGroup: t.runGroup,
|
|
8745
|
-
project
|
|
8789
|
+
project,
|
|
8746
8790
|
timestamp: r.startedAt,
|
|
8747
8791
|
run: {
|
|
8748
8792
|
test: r.test,
|
|
@@ -8836,7 +8880,7 @@ function parseArgs(args) {
|
|
|
8836
8880
|
json: false,
|
|
8837
8881
|
noUpload: false,
|
|
8838
8882
|
vars: {},
|
|
8839
|
-
claudeBin:
|
|
8883
|
+
claudeBin: "claude"
|
|
8840
8884
|
};
|
|
8841
8885
|
for (let i = 0;i < args.length; i++) {
|
|
8842
8886
|
const a = args[i];
|
|
@@ -9031,7 +9075,10 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9031
9075
|
cwd: workdir,
|
|
9032
9076
|
env: {
|
|
9033
9077
|
...claudeEnv(opts.configDir, env, homeEnvFile),
|
|
9034
|
-
PATH: opts.stepEnv.PATH
|
|
9078
|
+
PATH: opts.stepEnv.PATH,
|
|
9079
|
+
FIREGOAT_OUT: opts.out,
|
|
9080
|
+
FIREGOAT_WORKDIR: workdir,
|
|
9081
|
+
FIREGOAT_HOME: r.home
|
|
9035
9082
|
},
|
|
9036
9083
|
stderr,
|
|
9037
9084
|
timeoutMs: test.timeout * 1000,
|
|
@@ -9161,7 +9208,7 @@ async function evalCommand(args, root) {
|
|
|
9161
9208
|
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`);
|
|
9162
9209
|
const claudePath = resolveBin(opts.claudeBin);
|
|
9163
9210
|
if (!claudePath)
|
|
9164
|
-
return fatal(EXIT.notGraded, `Not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH
|
|
9211
|
+
return fatal(EXIT.notGraded, `Not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH.`);
|
|
9165
9212
|
let repo;
|
|
9166
9213
|
try {
|
|
9167
9214
|
repo = gitRepo(root);
|
|
@@ -9183,7 +9230,7 @@ async function evalCommand(args, root) {
|
|
|
9183
9230
|
const runId = newRunId();
|
|
9184
9231
|
let upload;
|
|
9185
9232
|
try {
|
|
9186
|
-
const pre = await uploadPreflight(opts, ui, basename3(repo?.mainCheckout ?? repo?.toplevel ?? root)
|
|
9233
|
+
const pre = await uploadPreflight(opts, ui, basename3(repo?.mainCheckout ?? repo?.toplevel ?? root), runId, repo?.info ?? null);
|
|
9187
9234
|
if (pre && "exitCode" in pre)
|
|
9188
9235
|
return fatal(pre.exitCode, pre.error);
|
|
9189
9236
|
upload = pre;
|
|
@@ -9266,7 +9313,7 @@ async function evalCommand(args, root) {
|
|
|
9266
9313
|
results[i] = r;
|
|
9267
9314
|
Object.assign(row, {
|
|
9268
9315
|
state: r.status,
|
|
9269
|
-
|
|
9316
|
+
reportedTurns: r.num_turns,
|
|
9270
9317
|
reason: r.reason,
|
|
9271
9318
|
checks: r.checks,
|
|
9272
9319
|
cost: r.cost,
|
|
@@ -9314,7 +9361,7 @@ async function evalCommand(args, root) {
|
|
|
9314
9361
|
passed: totals.pass,
|
|
9315
9362
|
failed: totals.fail,
|
|
9316
9363
|
notGraded: totals.error,
|
|
9317
|
-
cost: results.reduce((sum, r) => sum +
|
|
9364
|
+
cost: results.every((r) => r.cost !== null) ? results.reduce((sum, r) => sum + r.cost, 0) : null,
|
|
9318
9365
|
uploaded: results.filter((r) => r.upload?.status === "uploaded").length,
|
|
9319
9366
|
uploadFailed: uploadsFailed
|
|
9320
9367
|
})}
|