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.
Files changed (3) hide show
  1. package/README.md +18 -3
  2. package/dist/cli.js +121 -37
  3. 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` and checks 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:`.
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. In `.firegoat/`, git ignores everything except `fixtures/` and its own `.gitignore`: keep test fixtures (scripts, sample data) in `.firegoat/fixtures/` and commit them and `.firegoat/.gitignore` with `firegoat.yaml`.
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.17.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, ["name", "prompt", "end_goal", "checks", "timeout", "env"]);
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. In .firegoat/, git ignores everything but fixtures/
7843
- and the .gitignore: keep test fixtures (helper scripts, sample data) in
7844
- .firegoat/fixtures/ and commit them, and .firegoat/.gitignore, with firegoat.yaml.
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 and
7864
- checks of the same run; values are strings and can
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
- .firegoat/fixtures/ and copy them in build or install. The build and install
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 checks
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}], and paths: workdir,
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
- if (opts.install.length > 0) {
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: opts.install,
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 done("error", `not graded: ${error}`);
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 done("error", `not graded: ${outcome.reason}`);
8890
+ return ["error", `not graded: ${outcome.reason}`];
8829
8891
  if (!r.session)
8830
- return done("error", `not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`);
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 done("fail", failed.map((c) => `check failed (${c.error ?? `exit ${c.code}`}): ${c.run}`).join("; "));
8851
- return done("pass", test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`);
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 .firegoat/fixtures/ (committed), not inline here.
9572
- # The build step runs in this folder, so: build: cp -R .firegoat/fixtures/. "$FIREGOAT_OUT"
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: env vars for the agent (and for install and checks of the same
9576
- # run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/firegoat-ai/firegoat-cli.git"