firegoat 0.17.1 → 0.18.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 -3
- package/dist/cli.js +131 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,21 +130,36 @@ tests:
|
|
|
130
130
|
- run: ls "$ACME_HOME"/sessions | grep -q .
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
`env:` goes to the agent, and to `install
|
|
133
|
+
`env:` goes to the agent, and to `install`, checks and `cleanup` of the same run, so checks see the same values. Placeholders: `{{vars.KEY}}`, `{{run.id}}` (the eval's run id), `{{run.n}}` (1..runs, or 1..runs×models), `{{test.name}}`, and `$FIREGOAT_OUT` / `$FIREGOAT_WORKDIR` (also `${...}`), the same dirs `build`, `install` and checks see. There is no other `$` expansion: `$HOME` stays the literal text `$HOME`. `build` runs once, before any run: it gets only the top-level entries that use nothing but `{{vars.*}}`, `{{run.id}}` and `$FIREGOAT_OUT`. Values are strings (quote numbers). `PATH`, `CLAUDE_CONFIG_DIR` and `FIREGOAT_*` are firegoat's and can't be set; `ANTHROPIC_MODEL` neither: choose the model with `model:`.
|
|
134
134
|
|
|
135
135
|
Setting `HOME`: on macOS claude reads its login from `$HOME/Library`, so with a per-run `HOME` every run says "Not logged in" unless `install` runs `ln -s <real home>/Library "$HOME/Library"`. Get the real home in `build`, which doesn't get entries using `{{test.name}}` or `{{run.n}}` (e.g. `build: echo "$HOME" > "$FIREGOAT_OUT/.real-home"`).
|
|
136
136
|
|
|
137
137
|
## What eval does
|
|
138
138
|
|
|
139
139
|
- 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. claude saves its session there, in its own format.
|
|
140
|
-
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine.
|
|
140
|
+
- 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) outside it, e.g. in `fixtures/` next to `firegoat.yaml`, and commit them.
|
|
141
141
|
- 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.
|
|
142
142
|
- 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.
|
|
143
143
|
- Runs (tests × models × `runs`) go 4 at a time, each in its own folder with its own session; `--parallel N` changes that, `--parallel 1` runs them one after another. `summary.json` lists them in `firegoat.yaml` order.
|
|
144
144
|
- 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.
|
|
145
145
|
- 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/`, 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.
|
|
146
146
|
|
|
147
|
-
`firegoat.yaml` keys: `runs`, `model`, `build`, `install`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
147
|
+
`firegoat.yaml` keys: `runs`, `model`, `build`, `install`, `cleanup`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`, `install`, `cleanup`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
148
|
+
|
|
149
|
+
### Stop what the agent left running, seed one test
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
install: npm init -y >/dev/null
|
|
153
|
+
cleanup: pkill -f "$FIREGOAT_WORKDIR" || true # A dev server the agent started
|
|
154
|
+
tests:
|
|
155
|
+
- name: migrate
|
|
156
|
+
prompt: Migrate the app in this folder to acme v2 and start it.
|
|
157
|
+
end_goal: The app runs on acme v2.
|
|
158
|
+
install: cp -R "$FIREGOAT_OUT"/v1-app/. . # This test only, after the top-level install
|
|
159
|
+
cleanup: acme down || true # This test only, before the top-level cleanup
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Step order: `build` once per eval, in your folder; then per run, in its folder: `install` (top-level, then the test's), the agent, checks, `cleanup` (the test's, then top-level). `cleanup` runs whenever the run's folder exists: after the checks, and also after a failed install, a claude error or a timeout. Every cleanup step runs; a failing one prints a warning and is recorded in `run.json` (`cleanup: [{run, code}]`, output in `cleanup.txt`), but never changes the run's status or eval's exit code.
|
|
148
163
|
|
|
149
164
|
Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See [`examples/local-npm-registry`](examples/local-npm-registry/README.md).
|
|
150
165
|
|
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.18.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7078,6 +7078,7 @@ function parseConfig(text) {
|
|
|
7078
7078
|
"model",
|
|
7079
7079
|
"build",
|
|
7080
7080
|
"install",
|
|
7081
|
+
"cleanup",
|
|
7081
7082
|
"vars",
|
|
7082
7083
|
"env",
|
|
7083
7084
|
"tests"
|
|
@@ -7086,6 +7087,7 @@ function parseConfig(text) {
|
|
|
7086
7087
|
const models = parseModels(raw.model);
|
|
7087
7088
|
const build = commands(raw.build, "build");
|
|
7088
7089
|
const install = commands(raw.install, "install");
|
|
7090
|
+
const cleanup = commands(raw.cleanup, "cleanup");
|
|
7089
7091
|
const vars = parseVars(raw.vars);
|
|
7090
7092
|
const env = parseEnv(raw.env, "env");
|
|
7091
7093
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
@@ -7095,7 +7097,16 @@ function parseConfig(text) {
|
|
|
7095
7097
|
const p = `tests[${i}]`;
|
|
7096
7098
|
if (!isRecord(t))
|
|
7097
7099
|
fail(p, "must be a mapping");
|
|
7098
|
-
onlyKeys(t, p, [
|
|
7100
|
+
onlyKeys(t, p, [
|
|
7101
|
+
"name",
|
|
7102
|
+
"prompt",
|
|
7103
|
+
"end_goal",
|
|
7104
|
+
"checks",
|
|
7105
|
+
"timeout",
|
|
7106
|
+
"env",
|
|
7107
|
+
"install",
|
|
7108
|
+
"cleanup"
|
|
7109
|
+
]);
|
|
7099
7110
|
const name = str(t.name, `${p}.name`);
|
|
7100
7111
|
if (!NAME.test(name))
|
|
7101
7112
|
fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
|
|
@@ -7119,7 +7130,9 @@ function parseConfig(text) {
|
|
|
7119
7130
|
end_goal: str(t.end_goal, `${p}.end_goal`),
|
|
7120
7131
|
checks,
|
|
7121
7132
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
7122
|
-
env: { ...env, ...parseEnv(t.env, `${p}.env`) }
|
|
7133
|
+
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
7134
|
+
install: commands(t.install, `${p}.install`),
|
|
7135
|
+
cleanup: commands(t.cleanup, `${p}.cleanup`)
|
|
7123
7136
|
};
|
|
7124
7137
|
});
|
|
7125
7138
|
return {
|
|
@@ -7127,6 +7140,7 @@ function parseConfig(text) {
|
|
|
7127
7140
|
models,
|
|
7128
7141
|
build,
|
|
7129
7142
|
install,
|
|
7143
|
+
cleanup,
|
|
7130
7144
|
vars,
|
|
7131
7145
|
env,
|
|
7132
7146
|
tests
|
|
@@ -7252,7 +7266,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
7252
7266
|
import {
|
|
7253
7267
|
appendFileSync,
|
|
7254
7268
|
createWriteStream as createWriteStream2,
|
|
7255
|
-
existsSync as
|
|
7269
|
+
existsSync as existsSync3,
|
|
7256
7270
|
mkdirSync as mkdirSync3,
|
|
7257
7271
|
readdirSync as readdirSync2,
|
|
7258
7272
|
writeFileSync as writeFileSync3
|
|
@@ -7732,28 +7746,14 @@ function resolveKey(base) {
|
|
|
7732
7746
|
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7733
7747
|
|
|
7734
7748
|
// src/firegoat-dir.ts
|
|
7735
|
-
import {
|
|
7749
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
7736
7750
|
import { join as join2 } from "node:path";
|
|
7737
|
-
var GITIGNORE =
|
|
7738
|
-
.claude/
|
|
7739
|
-
`;
|
|
7740
|
-
var README = `# .firegoat
|
|
7741
|
-
|
|
7742
|
-
Files for the evals in firegoat.yaml (run them with \`npx firegoat eval\`).
|
|
7743
|
-
|
|
7744
|
-
- \`fixtures/\`: test fixtures (helper scripts, sample data) for build:, install: and checks. Commit them.
|
|
7745
|
-
- \`runs/\`: one folder per eval (transcripts, sessions, check output). Ignored by git.
|
|
7746
|
-
- \`.claude/\`: the empty claude config eval runs the agent with. Ignored by git.
|
|
7747
|
-
|
|
7748
|
-
Reference: \`npx firegoat init --help\`.
|
|
7751
|
+
var GITIGNORE = `*
|
|
7749
7752
|
`;
|
|
7750
7753
|
function ensureFiregoatDir(root) {
|
|
7751
7754
|
const dir = join2(root, ".firegoat");
|
|
7752
|
-
mkdirSync2(
|
|
7755
|
+
mkdirSync2(dir, { recursive: true });
|
|
7753
7756
|
writeFileSync(join2(dir, ".gitignore"), GITIGNORE);
|
|
7754
|
-
const readme = join2(dir, "README.md");
|
|
7755
|
-
if (!existsSync2(readme))
|
|
7756
|
-
writeFileSync(readme, README);
|
|
7757
7757
|
return dir;
|
|
7758
7758
|
}
|
|
7759
7759
|
|
|
@@ -7822,7 +7822,7 @@ var EXIT_CODES = `Exit codes:
|
|
|
7822
7822
|
3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
|
|
7823
7823
|
firegoat.ai answered 401/403 for the upload key); any other failure of
|
|
7824
7824
|
that key check exits 2
|
|
7825
|
-
Upload failures don't change the exit code.`;
|
|
7825
|
+
Upload and cleanup failures don't change the exit code.`;
|
|
7826
7826
|
function topHelp(version) {
|
|
7827
7827
|
return `firegoat ${version}: local AX evals. Your own claude gets one prompt in a fresh
|
|
7828
7828
|
folder, with none of your claude config; firegoat saves the session and runs your
|
|
@@ -7850,9 +7850,9 @@ In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs
|
|
|
7850
7850
|
firegoat-onboarding skill globally, into ~/.claude/skills/ (npx skills add
|
|
7851
7851
|
firegoat-ai/skills -g; in a terminal it asks: global, this repo, or none), offers
|
|
7852
7852
|
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
7853
|
-
paste into your coding agent.
|
|
7854
|
-
|
|
7855
|
-
|
|
7853
|
+
paste into your coding agent. .firegoat/ holds local run output and ignores itself
|
|
7854
|
+
in git. Keep test fixtures (helper scripts, sample data) outside it, e.g. in
|
|
7855
|
+
fixtures/ next to firegoat.yaml, and commit them.
|
|
7856
7856
|
|
|
7857
7857
|
--force Replace an existing firegoat.yaml
|
|
7858
7858
|
--local-skills Install the skill into this repo's .claude/skills/ instead
|
|
@@ -7868,11 +7868,17 @@ firegoat.yaml reference:
|
|
|
7868
7868
|
--model, claude's default. eval --model overrides
|
|
7869
7869
|
build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
|
|
7870
7870
|
$FIREGOAT_OUT is first on the agent's PATH
|
|
7871
|
-
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts
|
|
7871
|
+
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
|
|
7872
|
+
failure: that run is not graded
|
|
7873
|
+
cleanup: <sh> | [<sh>, ...] In each run's folder once the run is over: after the
|
|
7874
|
+
checks, and also after an error or timeout. Stops
|
|
7875
|
+
what the agent left running (servers, daemons).
|
|
7876
|
+
Every step runs; a failure is a warning (run.json
|
|
7877
|
+
cleanup, <test>/<n>/cleanup.txt), never the verdict
|
|
7872
7878
|
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts and checks;
|
|
7873
7879
|
--var KEY=value overrides
|
|
7874
|
-
env: {NAME: value} Env vars for the agent, and for install
|
|
7875
|
-
|
|
7880
|
+
env: {NAME: value} Env vars for the agent, and for install, checks
|
|
7881
|
+
and cleanup of the same run; values are strings and can
|
|
7876
7882
|
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
7877
7883
|
{{test.name}}, $FIREGOAT_OUT, $FIREGOAT_WORKDIR
|
|
7878
7884
|
(no other $ expansion: $HOME stays literal).
|
|
@@ -7897,10 +7903,15 @@ firegoat.yaml reference:
|
|
|
7897
7903
|
- run: ... Afterwards; exit 0 = pass; they set eval's exit code
|
|
7898
7904
|
timeout: 600 Optional, seconds
|
|
7899
7905
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
7906
|
+
install: <sh> | [...] Optional, runs after the top-level install
|
|
7907
|
+
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
7908
|
+
|
|
7909
|
+
Step order: build once per eval; then per run: install (top-level, then the
|
|
7910
|
+
test's), the agent, checks, cleanup (the test's, then top-level).
|
|
7900
7911
|
|
|
7901
7912
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7902
|
-
.
|
|
7903
|
-
commands and the checks get the vars as env vars, plus $FIREGOAT_OUT and
|
|
7913
|
+
e.g. fixtures/ (committed) and copy them in build or install. The build, install
|
|
7914
|
+
and cleanup commands and the checks get the vars as env vars, plus $FIREGOAT_OUT and
|
|
7904
7915
|
$FIREGOAT_WORKDIR. Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
7905
7916
|
transcript, greppable line by line: "[1] $ <command>" on every line of a shell
|
|
7906
7917
|
call (one line per line of the command, same n), "[2] Write <path>" for other
|
|
@@ -7917,6 +7928,7 @@ firegoat.yaml reference:
|
|
|
7917
7928
|
Example:
|
|
7918
7929
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7919
7930
|
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7931
|
+
cleanup: pkill -f "$FIREGOAT_WORKDIR" || true
|
|
7920
7932
|
tests:
|
|
7921
7933
|
- name: first-call
|
|
7922
7934
|
prompt: >-
|
|
@@ -7973,6 +7985,12 @@ Runs each test in firegoat.yaml (in the current folder): your claude gets the pr
|
|
|
7973
7985
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
|
|
7974
7986
|
outputs on PATH. Then the test's checks run in that folder.
|
|
7975
7987
|
|
|
7988
|
+
Step order: build: once, in this folder; then per run, in its folder: install:
|
|
7989
|
+
(top-level, then the test's), the agent, checks, cleanup: (the test's, then
|
|
7990
|
+
top-level). cleanup: runs whenever the folder exists, also after a failed install,
|
|
7991
|
+
a claude error or a timeout; a failing cleanup step prints a warning and never
|
|
7992
|
+
changes the run's status or the exit code.
|
|
7993
|
+
|
|
7976
7994
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
7977
7995
|
with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
|
|
7978
7996
|
plugins, skills or MCP servers. claude saves its session there. There is no
|
|
@@ -7989,7 +8007,8 @@ fixtures and build: stay in the current folder.
|
|
|
7989
8007
|
--parallel N Runs at once (default 4; 1 runs them one after another).
|
|
7990
8008
|
Tools with global on-disk state collide: give each run its
|
|
7991
8009
|
own home via env: ({{run.id}}-{{test.name}}-{{run.n}}) or use 1
|
|
7992
|
-
--var KEY=value Fills {{vars.KEY}}; env for build, install and
|
|
8010
|
+
--var KEY=value Fills {{vars.KEY}}; env for build, install, checks and
|
|
8011
|
+
cleanup
|
|
7993
8012
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
7994
8013
|
stops early); progress goes to stderr
|
|
7995
8014
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
@@ -8016,16 +8035,17 @@ Output: .firegoat/runs/<runId>/
|
|
|
8016
8035
|
<test>/<n>/run.json One run: status and reason (from the checks), model
|
|
8017
8036
|
(asked for, or null) and claudeModel (what claude's
|
|
8018
8037
|
init line reported), prompt, end_goal,
|
|
8019
|
-
checks [{run, code}],
|
|
8038
|
+
checks [{run, code}], cleanup [{run, code}],
|
|
8039
|
+
and paths: workdir,
|
|
8020
8040
|
transcript, session (claude's session file), stderr,
|
|
8021
|
-
checksLog; startedAt; upload ({status: "uploaded",
|
|
8041
|
+
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
8022
8042
|
url} | {status: "failed", error} | {status:
|
|
8023
8043
|
"skipped", reason}, null when not uploading)
|
|
8024
8044
|
<test>/<n>/transcript.txt What the agent did: each tool call with its command
|
|
8025
8045
|
("[n] $ " on every line of a shell call), short output
|
|
8026
8046
|
(indented), its text and final answer ("agent: " on
|
|
8027
8047
|
every line), files it made
|
|
8028
|
-
build.txt, <test>/<n>/install.txt, checks.txt Logs
|
|
8048
|
+
build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
|
|
8029
8049
|
|
|
8030
8050
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
8031
8051
|
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
@@ -8085,6 +8105,23 @@ async function runSteps(opts) {
|
|
|
8085
8105
|
await new Promise((r) => log.end(r));
|
|
8086
8106
|
}
|
|
8087
8107
|
}
|
|
8108
|
+
async function runAllSteps(opts) {
|
|
8109
|
+
const log = createWriteStream(opts.logPath);
|
|
8110
|
+
const outcomes = [];
|
|
8111
|
+
try {
|
|
8112
|
+
for (const run of opts.commands)
|
|
8113
|
+
outcomes.push(await runShell({
|
|
8114
|
+
run,
|
|
8115
|
+
cwd: opts.cwd,
|
|
8116
|
+
env: opts.env,
|
|
8117
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
8118
|
+
log
|
|
8119
|
+
}));
|
|
8120
|
+
return outcomes;
|
|
8121
|
+
} finally {
|
|
8122
|
+
await new Promise((r) => log.end(r));
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8088
8125
|
|
|
8089
8126
|
// src/transcript.ts
|
|
8090
8127
|
import { readdirSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -8459,7 +8496,7 @@ function ttyUI(out, opts) {
|
|
|
8459
8496
|
}
|
|
8460
8497
|
|
|
8461
8498
|
// src/upload.ts
|
|
8462
|
-
import { existsSync as
|
|
8499
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
8463
8500
|
import { basename as basename2 } from "node:path";
|
|
8464
8501
|
var MAX_FILE = 20 * 1024 * 1024;
|
|
8465
8502
|
var MAX_CHECKS = 200;
|
|
@@ -8535,7 +8572,7 @@ async function uploadRun(t, r) {
|
|
|
8535
8572
|
};
|
|
8536
8573
|
const bytes = {};
|
|
8537
8574
|
for (const name of FILES) {
|
|
8538
|
-
const b =
|
|
8575
|
+
const b = existsSync2(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
|
|
8539
8576
|
if (b.byteLength > MAX_FILE)
|
|
8540
8577
|
return {
|
|
8541
8578
|
status: "failed",
|
|
@@ -8704,11 +8741,11 @@ function parseArgs(args) {
|
|
|
8704
8741
|
}
|
|
8705
8742
|
function findSession(configDir, sessionId) {
|
|
8706
8743
|
const projects = join5(configDir, "projects");
|
|
8707
|
-
if (!
|
|
8744
|
+
if (!existsSync3(projects))
|
|
8708
8745
|
return null;
|
|
8709
8746
|
for (const d of readdirSync2(projects)) {
|
|
8710
8747
|
const f = join5(projects, d, `${sessionId}.jsonl`);
|
|
8711
|
-
if (
|
|
8748
|
+
if (existsSync3(f))
|
|
8712
8749
|
return f;
|
|
8713
8750
|
}
|
|
8714
8751
|
return null;
|
|
@@ -8779,19 +8816,14 @@ async function runOne(opts) {
|
|
|
8779
8816
|
transcript: join5(dir, "transcript.txt"),
|
|
8780
8817
|
stderr: join5(dir, "stderr.txt"),
|
|
8781
8818
|
checksLog: join5(dir, "checks.txt"),
|
|
8819
|
+
cleanup: [],
|
|
8820
|
+
cleanupLog: join5(dir, "cleanup.txt"),
|
|
8782
8821
|
num_turns: null,
|
|
8783
8822
|
cost: null,
|
|
8784
8823
|
duration_ms: 0,
|
|
8785
8824
|
startedAt: new Date(started).toISOString(),
|
|
8786
8825
|
upload: null
|
|
8787
8826
|
};
|
|
8788
|
-
const done = (status, reason) => {
|
|
8789
|
-
r.status = status;
|
|
8790
|
-
r.reason = reason;
|
|
8791
|
-
r.duration_ms = Date.now() - started;
|
|
8792
|
-
writeRunJson(r);
|
|
8793
|
-
return r;
|
|
8794
|
-
};
|
|
8795
8827
|
const env = renderEnv(test.env, opts.vars, {
|
|
8796
8828
|
"run.id": opts.runId,
|
|
8797
8829
|
"run.n": String(n),
|
|
@@ -8800,16 +8832,35 @@ async function runOne(opts) {
|
|
|
8800
8832
|
FIREGOAT_WORKDIR: workdir
|
|
8801
8833
|
});
|
|
8802
8834
|
const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
|
|
8803
|
-
|
|
8835
|
+
const [status, reason] = await attempt(opts, r, env, stepEnv);
|
|
8836
|
+
const cleanup = [...test.cleanup, ...opts.cleanup];
|
|
8837
|
+
if (cleanup.length > 0)
|
|
8838
|
+
r.cleanup = await runAllSteps({
|
|
8839
|
+
commands: cleanup,
|
|
8840
|
+
cwd: workdir,
|
|
8841
|
+
env: stepEnv,
|
|
8842
|
+
logPath: r.cleanupLog
|
|
8843
|
+
});
|
|
8844
|
+
r.status = status;
|
|
8845
|
+
r.reason = reason;
|
|
8846
|
+
r.duration_ms = Date.now() - started;
|
|
8847
|
+
writeRunJson(r);
|
|
8848
|
+
return r;
|
|
8849
|
+
}
|
|
8850
|
+
async function attempt(opts, r, env, stepEnv) {
|
|
8851
|
+
const { test, n } = opts;
|
|
8852
|
+
const { dir, workdir } = r;
|
|
8853
|
+
const install = [...opts.install, ...test.install];
|
|
8854
|
+
if (install.length > 0) {
|
|
8804
8855
|
const error = await runSteps({
|
|
8805
8856
|
what: "install",
|
|
8806
|
-
commands:
|
|
8857
|
+
commands: install,
|
|
8807
8858
|
cwd: workdir,
|
|
8808
8859
|
env: stepEnv,
|
|
8809
8860
|
logPath: join5(dir, "install.txt")
|
|
8810
8861
|
});
|
|
8811
8862
|
if (error)
|
|
8812
|
-
return
|
|
8863
|
+
return ["error", `not graded: ${error}`];
|
|
8813
8864
|
}
|
|
8814
8865
|
const stderr = createWriteStream2(r.stderr);
|
|
8815
8866
|
const outcome = await runClaude({
|
|
@@ -8836,9 +8887,12 @@ async function runOne(opts) {
|
|
|
8836
8887
|
r.num_turns = outcome.result?.num_turns ?? null;
|
|
8837
8888
|
r.cost = outcome.result?.total_cost_usd ?? null;
|
|
8838
8889
|
if (!outcome.ok)
|
|
8839
|
-
return
|
|
8890
|
+
return ["error", `not graded: ${outcome.reason}`];
|
|
8840
8891
|
if (!r.session)
|
|
8841
|
-
return
|
|
8892
|
+
return [
|
|
8893
|
+
"error",
|
|
8894
|
+
`not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`
|
|
8895
|
+
];
|
|
8842
8896
|
const log = createWriteStream2(r.checksLog);
|
|
8843
8897
|
try {
|
|
8844
8898
|
for (const c of test.checks)
|
|
@@ -8858,8 +8912,14 @@ async function runOne(opts) {
|
|
|
8858
8912
|
}
|
|
8859
8913
|
const failed = r.checks.filter((c) => c.code !== 0);
|
|
8860
8914
|
if (failed.length > 0)
|
|
8861
|
-
return
|
|
8862
|
-
|
|
8915
|
+
return [
|
|
8916
|
+
"fail",
|
|
8917
|
+
failed.map((c) => `check failed (${c.error ?? `exit ${c.code}`}): ${c.run}`).join("; ")
|
|
8918
|
+
];
|
|
8919
|
+
return [
|
|
8920
|
+
"pass",
|
|
8921
|
+
test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
|
|
8922
|
+
];
|
|
8863
8923
|
}
|
|
8864
8924
|
async function evalCommand(args, root) {
|
|
8865
8925
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -9011,6 +9071,7 @@ async function evalCommand(args, root) {
|
|
|
9011
9071
|
claudeBin: claudePath,
|
|
9012
9072
|
configDir,
|
|
9013
9073
|
install: config.install,
|
|
9074
|
+
cleanup: config.cleanup,
|
|
9014
9075
|
out,
|
|
9015
9076
|
stepEnv,
|
|
9016
9077
|
vars
|
|
@@ -9076,6 +9137,10 @@ async function evalCommand(args, root) {
|
|
|
9076
9137
|
rows,
|
|
9077
9138
|
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
9078
9139
|
});
|
|
9140
|
+
for (const r of results)
|
|
9141
|
+
for (const c of r.cleanup)
|
|
9142
|
+
if (c.code !== 0)
|
|
9143
|
+
console.error(`firegoat eval: Warning: ${r.test}#${r.n} cleanup failed (${c.error ?? `exit ${c.code}`}): ${c.run} (log: ${r.cleanupLog})`);
|
|
9079
9144
|
if (uploadsFailed > 0)
|
|
9080
9145
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
9081
9146
|
if (opts.json)
|
|
@@ -9352,7 +9417,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
9352
9417
|
|
|
9353
9418
|
// src/init.ts
|
|
9354
9419
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9355
|
-
import { existsSync as
|
|
9420
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9356
9421
|
import { basename as basename4, join as join6 } from "node:path";
|
|
9357
9422
|
import { createInterface } from "node:readline/promises";
|
|
9358
9423
|
|
|
@@ -9579,12 +9644,20 @@ runs: 1
|
|
|
9579
9644
|
# (An npm/pypi CLI that users run via npx/uvx: pack + install it as above instead;
|
|
9580
9645
|
# a binary on PATH does not stop "npx mycli" from fetching the registry release)
|
|
9581
9646
|
#
|
|
9582
|
-
# Helper files (scripts, sample data) go in .
|
|
9583
|
-
# The build step runs in this folder, so: build: cp -R
|
|
9647
|
+
# Helper files (scripts, sample data) go in e.g. fixtures/ (committed), not inline here.
|
|
9648
|
+
# The build step runs in this folder, so: build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
9584
9649
|
# Then e.g. install: cp "$FIREGOAT_OUT/wizard.mjs" .
|
|
9585
9650
|
|
|
9586
|
-
# Optional:
|
|
9587
|
-
#
|
|
9651
|
+
# Optional: cleanup: runs in each run's folder once the run is over (after the
|
|
9652
|
+
# checks, also after an error or timeout), e.g. to stop a server the agent left
|
|
9653
|
+
# running. A failing cleanup step is a warning, never the verdict.
|
|
9654
|
+
# cleanup:
|
|
9655
|
+
# - pkill -f "$FIREGOAT_WORKDIR" || true
|
|
9656
|
+
#
|
|
9657
|
+
# A test can add its own install: (after this one) and cleanup: (before this one).
|
|
9658
|
+
|
|
9659
|
+
# Optional: env vars for the agent (and for install, checks and cleanup of the
|
|
9660
|
+
# same run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
|
|
9588
9661
|
# {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR (no
|
|
9589
9662
|
# other $ expansion: $HOME stays literal). A test can add its own env:.
|
|
9590
9663
|
# Runs go 4 at a time (--parallel), so give a tool with a global home (~/.mytool)
|
|
@@ -9726,7 +9799,7 @@ ${INIT_HELP}`);
|
|
|
9726
9799
|
}
|
|
9727
9800
|
ensureFiregoatDir(cwd);
|
|
9728
9801
|
const configPath = join6(cwd, CONFIG_FILE);
|
|
9729
|
-
const keep =
|
|
9802
|
+
const keep = existsSync4(configPath) && !force;
|
|
9730
9803
|
if (!keep)
|
|
9731
9804
|
writeFileSync4(configPath, template(basename4(cwd)));
|
|
9732
9805
|
if (interactive) {
|