firegoat 0.17.0 → 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 +121 -37
- 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
|
|
@@ -7735,9 +7749,6 @@ var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
|
7735
7749
|
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
7736
7750
|
import { join as join2 } from "node:path";
|
|
7737
7751
|
var GITIGNORE = `*
|
|
7738
|
-
!.gitignore
|
|
7739
|
-
!fixtures/
|
|
7740
|
-
!fixtures/**
|
|
7741
7752
|
`;
|
|
7742
7753
|
function ensureFiregoatDir(root) {
|
|
7743
7754
|
const dir = join2(root, ".firegoat");
|
|
@@ -7811,7 +7822,7 @@ var EXIT_CODES = `Exit codes:
|
|
|
7811
7822
|
3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
|
|
7812
7823
|
firegoat.ai answered 401/403 for the upload key); any other failure of
|
|
7813
7824
|
that key check exits 2
|
|
7814
|
-
Upload failures don't change the exit code.`;
|
|
7825
|
+
Upload and cleanup failures don't change the exit code.`;
|
|
7815
7826
|
function topHelp(version) {
|
|
7816
7827
|
return `firegoat ${version}: local AX evals. Your own claude gets one prompt in a fresh
|
|
7817
7828
|
folder, with none of your claude config; firegoat saves the session and runs your
|
|
@@ -7839,9 +7850,9 @@ In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs
|
|
|
7839
7850
|
firegoat-onboarding skill globally, into ~/.claude/skills/ (npx skills add
|
|
7840
7851
|
firegoat-ai/skills -g; in a terminal it asks: global, this repo, or none), offers
|
|
7841
7852
|
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
7842
|
-
paste into your coding agent.
|
|
7843
|
-
|
|
7844
|
-
|
|
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.
|
|
7845
7856
|
|
|
7846
7857
|
--force Replace an existing firegoat.yaml
|
|
7847
7858
|
--local-skills Install the skill into this repo's .claude/skills/ instead
|
|
@@ -7857,11 +7868,17 @@ firegoat.yaml reference:
|
|
|
7857
7868
|
--model, claude's default. eval --model overrides
|
|
7858
7869
|
build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
|
|
7859
7870
|
$FIREGOAT_OUT is first on the agent's PATH
|
|
7860
|
-
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
|
|
7861
7878
|
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts and checks;
|
|
7862
7879
|
--var KEY=value overrides
|
|
7863
|
-
env: {NAME: value} Env vars for the agent, and for install
|
|
7864
|
-
|
|
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
|
|
7865
7882
|
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
7866
7883
|
{{test.name}}, $FIREGOAT_OUT, $FIREGOAT_WORKDIR
|
|
7867
7884
|
(no other $ expansion: $HOME stays literal).
|
|
@@ -7886,10 +7903,15 @@ firegoat.yaml reference:
|
|
|
7886
7903
|
- run: ... Afterwards; exit 0 = pass; they set eval's exit code
|
|
7887
7904
|
timeout: 600 Optional, seconds
|
|
7888
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).
|
|
7889
7911
|
|
|
7890
7912
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7891
|
-
.
|
|
7892
|
-
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
|
|
7893
7915
|
$FIREGOAT_WORKDIR. Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
7894
7916
|
transcript, greppable line by line: "[1] $ <command>" on every line of a shell
|
|
7895
7917
|
call (one line per line of the command, same n), "[2] Write <path>" for other
|
|
@@ -7906,6 +7928,7 @@ firegoat.yaml reference:
|
|
|
7906
7928
|
Example:
|
|
7907
7929
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7908
7930
|
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7931
|
+
cleanup: pkill -f "$FIREGOAT_WORKDIR" || true
|
|
7909
7932
|
tests:
|
|
7910
7933
|
- name: first-call
|
|
7911
7934
|
prompt: >-
|
|
@@ -7962,6 +7985,12 @@ Runs each test in firegoat.yaml (in the current folder): your claude gets the pr
|
|
|
7962
7985
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
|
|
7963
7986
|
outputs on PATH. Then the test's checks run in that folder.
|
|
7964
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
|
+
|
|
7965
7994
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
7966
7995
|
with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
|
|
7967
7996
|
plugins, skills or MCP servers. claude saves its session there. There is no
|
|
@@ -7978,7 +8007,8 @@ fixtures and build: stay in the current folder.
|
|
|
7978
8007
|
--parallel N Runs at once (default 4; 1 runs them one after another).
|
|
7979
8008
|
Tools with global on-disk state collide: give each run its
|
|
7980
8009
|
own home via env: ({{run.id}}-{{test.name}}-{{run.n}}) or use 1
|
|
7981
|
-
--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
|
|
7982
8012
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
7983
8013
|
stops early); progress goes to stderr
|
|
7984
8014
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
@@ -8005,16 +8035,17 @@ Output: .firegoat/runs/<runId>/
|
|
|
8005
8035
|
<test>/<n>/run.json One run: status and reason (from the checks), model
|
|
8006
8036
|
(asked for, or null) and claudeModel (what claude's
|
|
8007
8037
|
init line reported), prompt, end_goal,
|
|
8008
|
-
checks [{run, code}],
|
|
8038
|
+
checks [{run, code}], cleanup [{run, code}],
|
|
8039
|
+
and paths: workdir,
|
|
8009
8040
|
transcript, session (claude's session file), stderr,
|
|
8010
|
-
checksLog; startedAt; upload ({status: "uploaded",
|
|
8041
|
+
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
8011
8042
|
url} | {status: "failed", error} | {status:
|
|
8012
8043
|
"skipped", reason}, null when not uploading)
|
|
8013
8044
|
<test>/<n>/transcript.txt What the agent did: each tool call with its command
|
|
8014
8045
|
("[n] $ " on every line of a shell call), short output
|
|
8015
8046
|
(indented), its text and final answer ("agent: " on
|
|
8016
8047
|
every line), files it made
|
|
8017
|
-
build.txt, <test>/<n>/install.txt, checks.txt Logs
|
|
8048
|
+
build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
|
|
8018
8049
|
|
|
8019
8050
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
8020
8051
|
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
@@ -8074,6 +8105,23 @@ async function runSteps(opts) {
|
|
|
8074
8105
|
await new Promise((r) => log.end(r));
|
|
8075
8106
|
}
|
|
8076
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
|
+
}
|
|
8077
8125
|
|
|
8078
8126
|
// src/transcript.ts
|
|
8079
8127
|
import { readdirSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -8768,19 +8816,14 @@ async function runOne(opts) {
|
|
|
8768
8816
|
transcript: join5(dir, "transcript.txt"),
|
|
8769
8817
|
stderr: join5(dir, "stderr.txt"),
|
|
8770
8818
|
checksLog: join5(dir, "checks.txt"),
|
|
8819
|
+
cleanup: [],
|
|
8820
|
+
cleanupLog: join5(dir, "cleanup.txt"),
|
|
8771
8821
|
num_turns: null,
|
|
8772
8822
|
cost: null,
|
|
8773
8823
|
duration_ms: 0,
|
|
8774
8824
|
startedAt: new Date(started).toISOString(),
|
|
8775
8825
|
upload: null
|
|
8776
8826
|
};
|
|
8777
|
-
const done = (status, reason) => {
|
|
8778
|
-
r.status = status;
|
|
8779
|
-
r.reason = reason;
|
|
8780
|
-
r.duration_ms = Date.now() - started;
|
|
8781
|
-
writeRunJson(r);
|
|
8782
|
-
return r;
|
|
8783
|
-
};
|
|
8784
8827
|
const env = renderEnv(test.env, opts.vars, {
|
|
8785
8828
|
"run.id": opts.runId,
|
|
8786
8829
|
"run.n": String(n),
|
|
@@ -8789,16 +8832,35 @@ async function runOne(opts) {
|
|
|
8789
8832
|
FIREGOAT_WORKDIR: workdir
|
|
8790
8833
|
});
|
|
8791
8834
|
const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
|
|
8792
|
-
|
|
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) {
|
|
8793
8855
|
const error = await runSteps({
|
|
8794
8856
|
what: "install",
|
|
8795
|
-
commands:
|
|
8857
|
+
commands: install,
|
|
8796
8858
|
cwd: workdir,
|
|
8797
8859
|
env: stepEnv,
|
|
8798
8860
|
logPath: join5(dir, "install.txt")
|
|
8799
8861
|
});
|
|
8800
8862
|
if (error)
|
|
8801
|
-
return
|
|
8863
|
+
return ["error", `not graded: ${error}`];
|
|
8802
8864
|
}
|
|
8803
8865
|
const stderr = createWriteStream2(r.stderr);
|
|
8804
8866
|
const outcome = await runClaude({
|
|
@@ -8825,9 +8887,12 @@ async function runOne(opts) {
|
|
|
8825
8887
|
r.num_turns = outcome.result?.num_turns ?? null;
|
|
8826
8888
|
r.cost = outcome.result?.total_cost_usd ?? null;
|
|
8827
8889
|
if (!outcome.ok)
|
|
8828
|
-
return
|
|
8890
|
+
return ["error", `not graded: ${outcome.reason}`];
|
|
8829
8891
|
if (!r.session)
|
|
8830
|
-
return
|
|
8892
|
+
return [
|
|
8893
|
+
"error",
|
|
8894
|
+
`not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`
|
|
8895
|
+
];
|
|
8831
8896
|
const log = createWriteStream2(r.checksLog);
|
|
8832
8897
|
try {
|
|
8833
8898
|
for (const c of test.checks)
|
|
@@ -8847,8 +8912,14 @@ async function runOne(opts) {
|
|
|
8847
8912
|
}
|
|
8848
8913
|
const failed = r.checks.filter((c) => c.code !== 0);
|
|
8849
8914
|
if (failed.length > 0)
|
|
8850
|
-
return
|
|
8851
|
-
|
|
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
|
+
];
|
|
8852
8923
|
}
|
|
8853
8924
|
async function evalCommand(args, root) {
|
|
8854
8925
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -9000,6 +9071,7 @@ async function evalCommand(args, root) {
|
|
|
9000
9071
|
claudeBin: claudePath,
|
|
9001
9072
|
configDir,
|
|
9002
9073
|
install: config.install,
|
|
9074
|
+
cleanup: config.cleanup,
|
|
9003
9075
|
out,
|
|
9004
9076
|
stepEnv,
|
|
9005
9077
|
vars
|
|
@@ -9065,6 +9137,10 @@ async function evalCommand(args, root) {
|
|
|
9065
9137
|
rows,
|
|
9066
9138
|
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
9067
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})`);
|
|
9068
9144
|
if (uploadsFailed > 0)
|
|
9069
9145
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
9070
9146
|
if (opts.json)
|
|
@@ -9568,12 +9644,20 @@ runs: 1
|
|
|
9568
9644
|
# (An npm/pypi CLI that users run via npx/uvx: pack + install it as above instead;
|
|
9569
9645
|
# a binary on PATH does not stop "npx mycli" from fetching the registry release)
|
|
9570
9646
|
#
|
|
9571
|
-
# Helper files (scripts, sample data) go in .
|
|
9572
|
-
# 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"
|
|
9573
9649
|
# Then e.g. install: cp "$FIREGOAT_OUT/wizard.mjs" .
|
|
9574
9650
|
|
|
9575
|
-
# Optional:
|
|
9576
|
-
#
|
|
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}},
|
|
9577
9661
|
# {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR (no
|
|
9578
9662
|
# other $ expansion: $HOME stays literal). A test can add its own env:.
|
|
9579
9663
|
# Runs go 4 at a time (--parallel), so give a tool with a global home (~/.mytool)
|