firegoat 0.17.1 → 0.19.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 +89 -13
  2. package/dist/cli.js +372 -143
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,12 +7,12 @@ Status: early. See `plan.md`.
7
7
  ## Quickstart
8
8
 
9
9
  ```bash
10
- npx firegoat init # Creates firegoat.yaml and .firegoat/, installs the firegoat-onboarding skill, copies a prompt for your agent
10
+ npx firegoat@latest init # Creates firegoat.yaml and .firegoat/, installs the firegoat-onboarding skill, copies a prompt for your agent
11
11
  # Edit firegoat.yaml (or paste the prompt into your coding agent and let it)
12
- npx firegoat eval # Runs your claude on each test, in .firegoat/runs/<runId>/<test>/<n>/work
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. `claude` must be installed and logged in (run `claude` once).
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 0.18.0`), 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
 
@@ -98,6 +98,21 @@ tests:
98
98
  npx firegoat eval --var SHA=$(git rev-parse HEAD)
99
99
  ```
100
100
 
101
+ ### Start from a template project
102
+
103
+ ```yaml
104
+ build: cp -R fixtures/starter-app "$FIREGOAT_OUT/starter-app"
105
+ install: cp -R "$FIREGOAT_OUT/starter-app/." .
106
+ tests:
107
+ - name: add-auth
108
+ prompt: Add acme login to this app. Keep the existing pages working.
109
+ end_goal: The app signs users in with acme, and the existing pages still render.
110
+ checks:
111
+ - run: npm test
112
+ ```
113
+
114
+ No template feature needed: keep the project committed in `fixtures/starter-app/`, `build` copies it once per eval, and `install` copies it into each fresh folder, so every run begins in its own copy of that project.
115
+
101
116
  ### Same tests on several models
102
117
 
103
118
  ```yaml
@@ -113,11 +128,43 @@ tests:
113
128
 
114
129
  `model:` is passed to claude as `--model`. A list is a matrix: each test runs `runs` times per model, numbered `1..runs×models` (here `quickstart/1`, `2` on haiku, `3`, `4` on sonnet). `npx firegoat eval --model claude-opus-5-5` replaces it for one eval; repeat the flag for a matrix. Unset, firegoat passes no `--model` and claude uses its default. Each run's `run.json` records `model` (what firegoat asked for, or null) and `claudeModel` (what claude reported starting with), and the output labels each run with its model.
115
130
 
116
- ### Env vars for the agent: a home dir per run, a feature flag
131
+ ### Compare two setups
132
+
133
+ ```yaml
134
+ build: npm pack --pack-destination "$FIREGOAT_OUT"
135
+ tests:
136
+ - name: deploy-local-build
137
+ prompt: &deploy Deploy this app to an acme preview environment and print the URL.
138
+ end_goal: &deployed The app runs on an acme preview URL, and the URL is printed.
139
+ install: npm install "$FIREGOAT_OUT"/acme-*.tgz
140
+ - name: deploy-npm-release
141
+ prompt: *deploy
142
+ end_goal: *deployed
143
+ install: npm install acme@latest
144
+ ```
145
+
146
+ There is no variants feature: to compare setups (your local build vs the npm release, with vs without `--chrome`, docs A vs docs B), write one test per setup with the same prompt and a different per-test `install:`, `env:` or `claude_args:`. A YAML anchor (`&deploy`) and alias (`*deploy`) keep the prompt in one place. Skills installed into the run's folder are not loaded as skills: eval runs claude with `--setting-sources ""` and `--disable-slash-commands`, so a with/without-skill comparison isn't possible yet.
147
+
148
+ ### Extra claude flags
149
+
150
+ ```yaml
151
+ claude_args: [--plugin-dir, ./plugins/acme] # Every test
152
+ tests:
153
+ - name: dashboard-key
154
+ prompt: Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put it in .env as ACME_API_KEY.
155
+ end_goal: .env holds a key copied from the dashboard, not a made-up one.
156
+ claude_args: [--chrome] # Claude in Chrome, this test only
157
+ ```
158
+
159
+ `claude_args:` (top level, and per test after the top-level ones) is passed to claude after firegoat's own flags and `--model`. Each entry is one argument, as-is, with no shell splitting: `[--plugin-dir, ./plugins/acme]`, not `["--plugin-dir ./plugins/acme"]`. Use it for `--chrome`, `--plugin-dir`, `--mcp-config`, `--agents`, `--append-system-prompt`. The flags firegoat sets itself are config errors, also as `--flag=value`: `-p`, `--print`, `--output-format`, `--verbose`, `--setting-sources`, `--strict-mcp-config`, `--disable-slash-commands`, `--dangerously-skip-permissions`, `--model` (use `model:`), and `--session-id`, `--resume`, `-r`, `--continue`, `-c` (every run is a fresh session). Each run's `run.json` records `claudeArgs`.
160
+
161
+ 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.
162
+
163
+ ### Env vars for the agent: a feature flag, a tool that ignores HOME
117
164
 
118
165
  ```yaml
119
166
  env:
120
- ACME_HOME: "/tmp/acme-{{run.id}}-{{run.n}}" # Sessions/config never collide across runs
167
+ ACME_HOME: "$FIREGOAT_HOME/.acme" # Only for a tool that ignores HOME
121
168
  ACME_TELEMETRY: "off"
122
169
  UV_FIND_LINKS: "$FIREGOAT_OUT" # Resolve the wheel build: made
123
170
  tests:
@@ -130,21 +177,50 @@ tests:
130
177
  - run: ls "$ACME_HOME"/sessions | grep -q .
131
178
  ```
132
179
 
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:`.
180
+ `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` / `$FIREGOAT_HOME` (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:`. `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME` and `CLAUDE_ENV_FILE` are reserved too: every run gets its own home.
181
+
182
+ ### Each run gets its own HOME
183
+
184
+ Every run has its own home, `.firegoat/runs/<runId>/<test>/<n>/home/`, with `.config`, `.cache`, `.local/share` and `.local/state` for the XDG dirs. A tool that keeps logins, config or caches in `~` starts fresh in every run, and parallel runs don't collide. Point a tool's own `*_HOME` var at `$FIREGOAT_HOME` only if the tool ignores `HOME`.
185
+
186
+ - The agent's shell commands see it: firegoat writes `<n>/home.env` (exporting `HOME` and `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`) and passes it to claude as `CLAUDE_ENV_FILE`, which claude sources before every Bash command. claude itself keeps your real `HOME`, where its login is (the macOS keychain lookup, `~/.claude/.credentials.json` on Linux).
187
+ - `install`, checks and `cleanup` get `HOME` and the XDG vars set to the run's home, plus `$FIREGOAT_HOME`. `build` keeps your real home.
188
+ - `run.json` records it as `home`.
134
189
 
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"`).
190
+ Limits:
191
+
192
+ - claude's own Read/Write/Edit tools expand `~` to your real home; only Bash commands get the run's home.
193
+ - Lookups by user id (`os.userInfo().homedir`, python's `pwd`, `getpwuid`) still return your real home.
194
+ - Keychain-backed logins (`gh`, `docker`, git credential helpers) look logged out inside a run. Log tools in from `install:`, or pass tokens with `env:`.
195
+ - The fresh home has no `.gitconfig`: if the agent commits, set `git config --global user.name ...` / `user.email` in `install:`.
196
+ - claude's shell snapshot still sources your real `~/.zshrc` (or `~/.bashrc`).
136
197
 
137
198
  ## What eval does
138
199
 
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. Commit `.firegoat/` with `firegoat.yaml`: its `.gitignore` ignores only `runs/` and `.claude/`, test fixtures (scripts, sample data) go in `.firegoat/fixtures/`, and its `README.md` says what each folder is.
200
+ - 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.
201
+ - 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
202
  - 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
- - 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
- - 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.
203
+ - 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`.
204
+ - 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.
144
205
  - 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
- - 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.
206
+ - 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.
207
+
208
+ `firegoat.yaml` keys: `runs`, `model`, `build`, `install`, `cleanup`, `vars`, `env`, `claude_args`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`, `install`, `cleanup`, `claude_args`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
209
+
210
+ ### Stop what the agent left running, seed one test
211
+
212
+ ```yaml
213
+ install: npm init -y >/dev/null
214
+ cleanup: pkill -f "$FIREGOAT_WORKDIR" || true # A dev server the agent started
215
+ tests:
216
+ - name: migrate
217
+ prompt: Migrate the app in this folder to acme v2 and start it.
218
+ end_goal: The app runs on acme v2.
219
+ install: cp -R "$FIREGOAT_OUT"/v1-app/. . # This test only, after the top-level install
220
+ cleanup: acme down || true # This test only, before the top-level cleanup
221
+ ```
146
222
 
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`.
223
+ 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
224
 
149
225
  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
226
 
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.1",
6950
+ version: "0.19.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
@@ -7078,16 +7078,20 @@ function parseConfig(text) {
7078
7078
  "model",
7079
7079
  "build",
7080
7080
  "install",
7081
+ "cleanup",
7081
7082
  "vars",
7082
7083
  "env",
7084
+ "claude_args",
7083
7085
  "tests"
7084
7086
  ]);
7085
7087
  const runs = posInt(raw.runs, "runs", 1);
7086
7088
  const models = parseModels(raw.model);
7087
7089
  const build = commands(raw.build, "build");
7088
7090
  const install = commands(raw.install, "install");
7091
+ const cleanup = commands(raw.cleanup, "cleanup");
7089
7092
  const vars = parseVars(raw.vars);
7090
7093
  const env = parseEnv(raw.env, "env");
7094
+ const claudeArgs = parseClaudeArgs(raw.claude_args, "claude_args");
7091
7095
  if (!Array.isArray(raw.tests) || raw.tests.length === 0)
7092
7096
  fail("tests", "must be a non-empty list");
7093
7097
  const names = new Set;
@@ -7095,7 +7099,17 @@ function parseConfig(text) {
7095
7099
  const p = `tests[${i}]`;
7096
7100
  if (!isRecord(t))
7097
7101
  fail(p, "must be a mapping");
7098
- onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout", "env"]);
7102
+ onlyKeys(t, p, [
7103
+ "name",
7104
+ "prompt",
7105
+ "end_goal",
7106
+ "checks",
7107
+ "timeout",
7108
+ "env",
7109
+ "install",
7110
+ "cleanup",
7111
+ "claude_args"
7112
+ ]);
7099
7113
  const name = str(t.name, `${p}.name`);
7100
7114
  if (!NAME.test(name))
7101
7115
  fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
@@ -7119,7 +7133,13 @@ function parseConfig(text) {
7119
7133
  end_goal: str(t.end_goal, `${p}.end_goal`),
7120
7134
  checks,
7121
7135
  timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
7122
- env: { ...env, ...parseEnv(t.env, `${p}.env`) }
7136
+ env: { ...env, ...parseEnv(t.env, `${p}.env`) },
7137
+ install: commands(t.install, `${p}.install`),
7138
+ cleanup: commands(t.cleanup, `${p}.cleanup`),
7139
+ claudeArgs: [
7140
+ ...claudeArgs,
7141
+ ...parseClaudeArgs(t.claude_args, `${p}.claude_args`)
7142
+ ]
7123
7143
  };
7124
7144
  });
7125
7145
  return {
@@ -7127,6 +7147,7 @@ function parseConfig(text) {
7127
7147
  models,
7128
7148
  build,
7129
7149
  install,
7150
+ cleanup,
7130
7151
  vars,
7131
7152
  env,
7132
7153
  tests
@@ -7158,6 +7179,41 @@ function parseModels(raw) {
7158
7179
  return name;
7159
7180
  });
7160
7181
  }
7182
+ var OWNED_FLAGS = [
7183
+ [
7184
+ [
7185
+ "-p",
7186
+ "--print",
7187
+ "--output-format",
7188
+ "--verbose",
7189
+ "--setting-sources",
7190
+ "--strict-mcp-config",
7191
+ "--disable-slash-commands",
7192
+ "--dangerously-skip-permissions"
7193
+ ],
7194
+ "firegoat sets it for every run"
7195
+ ],
7196
+ [["--model"], "choose the model with top-level model: (or eval --model)"],
7197
+ [
7198
+ ["--session-id", "--resume", "-r", "--continue", "-c"],
7199
+ "every run is a fresh session"
7200
+ ]
7201
+ ];
7202
+ function parseClaudeArgs(raw, path) {
7203
+ if (raw === undefined)
7204
+ return [];
7205
+ if (!Array.isArray(raw))
7206
+ fail(path, "must be a list of claude arguments, one per entry, e.g. [--chrome]");
7207
+ return raw.map((a, i) => {
7208
+ const p = `${path}[${i}]`;
7209
+ const arg = str(a, p);
7210
+ for (const [flags, why] of OWNED_FLAGS)
7211
+ for (const f of flags)
7212
+ if (arg === f || arg.startsWith(`${f}=`))
7213
+ fail(p, `${f} is not allowed: ${why}`);
7214
+ return arg;
7215
+ });
7216
+ }
7161
7217
  var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
7162
7218
  function parseVars(raw) {
7163
7219
  if (raw === undefined)
@@ -7179,10 +7235,18 @@ var RESERVED_ENV = new Set([
7179
7235
  "CLAUDE_CONFIG_DIR",
7180
7236
  "CLAUDE_SECURESTORAGE_CONFIG_DIR"
7181
7237
  ]);
7238
+ var HOME_ENV = new Set([
7239
+ "HOME",
7240
+ "XDG_CONFIG_HOME",
7241
+ "XDG_CACHE_HOME",
7242
+ "XDG_DATA_HOME",
7243
+ "XDG_STATE_HOME",
7244
+ "CLAUDE_ENV_FILE"
7245
+ ]);
7182
7246
  var RUN_PLACEHOLDERS = ["run.id", "run.n", "test.name"];
7183
7247
  var EVAL_PLACEHOLDERS = ["run.id"];
7184
7248
  var ANY_PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g;
7185
- var DIR_REF = /\$(?:\{(FIREGOAT_OUT|FIREGOAT_WORKDIR)\}|(FIREGOAT_OUT|FIREGOAT_WORKDIR)(?![A-Za-z0-9_]))/g;
7249
+ var DIR_REF = /\$(?:\{(FIREGOAT_OUT|FIREGOAT_WORKDIR|FIREGOAT_HOME)\}|(FIREGOAT_OUT|FIREGOAT_WORKDIR|FIREGOAT_HOME)(?![A-Za-z0-9_]))/g;
7186
7250
  var ENV_REF = new RegExp(`${ANY_PLACEHOLDER.source}|${DIR_REF.source}`, "g");
7187
7251
  function parseEnv(raw, path) {
7188
7252
  if (raw === undefined)
@@ -7196,6 +7260,8 @@ function parseEnv(raw, path) {
7196
7260
  fail(p, "name must be letters, digits and _, not starting with a digit");
7197
7261
  if (k === "ANTHROPIC_MODEL")
7198
7262
  fail(p, "is reserved: choose the model with top-level model: (or eval --model)");
7263
+ if (HOME_ENV.has(k))
7264
+ fail(p, "Reserved: every run gets its own home ($FIREGOAT_HOME)");
7199
7265
  if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
7200
7266
  fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
7201
7267
  if (typeof v !== "string")
@@ -7252,12 +7318,12 @@ import { randomBytes as randomBytes2 } from "node:crypto";
7252
7318
  import {
7253
7319
  appendFileSync,
7254
7320
  createWriteStream as createWriteStream2,
7255
- existsSync as existsSync4,
7256
- mkdirSync as mkdirSync3,
7321
+ existsSync as existsSync3,
7322
+ mkdirSync as mkdirSync4,
7257
7323
  readdirSync as readdirSync2,
7258
- writeFileSync as writeFileSync3
7324
+ writeFileSync as writeFileSync4
7259
7325
  } from "node:fs";
7260
- import { basename as basename3, delimiter as delimiter2, join as join5 } from "node:path";
7326
+ import { basename as basename3, delimiter as delimiter2, join as join6 } from "node:path";
7261
7327
 
7262
7328
  // src/claude.ts
7263
7329
  import { spawn, spawnSync } from "node:child_process";
@@ -7272,15 +7338,18 @@ var CLAUDE_FLAGS = [
7272
7338
  "--disable-slash-commands"
7273
7339
  ];
7274
7340
  var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
7341
+ var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
7342
+ var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
7275
7343
  function hostEnv() {
7276
- return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_")));
7344
+ return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
7277
7345
  }
7278
- function claudeEnv(configDir, env = {}) {
7346
+ function claudeEnv(configDir, env = {}, envFile) {
7279
7347
  return {
7280
7348
  ...hostEnv(),
7281
7349
  ...env,
7282
7350
  CLAUDE_CONFIG_DIR: configDir,
7283
- CLAUDE_SECURESTORAGE_CONFIG_DIR: ""
7351
+ CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
7352
+ ...envFile === undefined ? {} : { CLAUDE_ENV_FILE: envFile }
7284
7353
  };
7285
7354
  }
7286
7355
  function run(opts) {
@@ -7323,7 +7392,8 @@ async function runClaude(opts) {
7323
7392
  "-p",
7324
7393
  opts.prompt,
7325
7394
  ...CLAUDE_FLAGS,
7326
- ...opts.model === undefined ? [] : ["--model", opts.model]
7395
+ ...opts.model === undefined ? [] : ["--model", opts.model],
7396
+ ...opts.extraArgs
7327
7397
  ],
7328
7398
  cwd: opts.cwd,
7329
7399
  env: opts.env,
@@ -7732,28 +7802,14 @@ function resolveKey(base) {
7732
7802
  var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
7733
7803
 
7734
7804
  // src/firegoat-dir.ts
7735
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
7805
+ import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
7736
7806
  import { join as join2 } from "node:path";
7737
- var GITIGNORE = `runs/
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\`.
7807
+ var GITIGNORE = `*
7749
7808
  `;
7750
7809
  function ensureFiregoatDir(root) {
7751
7810
  const dir = join2(root, ".firegoat");
7752
- mkdirSync2(join2(dir, "fixtures"), { recursive: true });
7811
+ mkdirSync2(dir, { recursive: true });
7753
7812
  writeFileSync(join2(dir, ".gitignore"), GITIGNORE);
7754
- const readme = join2(dir, "README.md");
7755
- if (!existsSync2(readme))
7756
- writeFileSync(readme, README);
7757
7813
  return dir;
7758
7814
  }
7759
7815
 
@@ -7822,21 +7878,25 @@ var EXIT_CODES = `Exit codes:
7822
7878
  3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
7823
7879
  firegoat.ai answered 401/403 for the upload key); any other failure of
7824
7880
  that key check exits 2
7825
- Upload failures don't change the exit code.`;
7881
+ Upload and cleanup failures don't change the exit code.`;
7826
7882
  function topHelp(version) {
7827
7883
  return `firegoat ${version}: local AX evals. Your own claude gets one prompt in a fresh
7828
7884
  folder, with none of your claude config; firegoat saves the session and runs your
7829
7885
  script checks.
7830
7886
 
7831
- npx firegoat init Create firegoat.yaml and .firegoat/, install the skill
7832
- npx firegoat eval Run the tests (claude must be logged in: run \`claude\` once)
7833
- npx firegoat login Log in to firegoat.ai: eval then uploads each session there
7834
- npx firegoat logout Forget that login
7835
- npx firegoat feedback "<what got in the way>" Tell the firegoat team
7887
+ npx firegoat@latest init Create firegoat.yaml and .firegoat/, install the skill
7888
+ npx firegoat@latest eval Run the tests (claude must be logged in: run \`claude\`)
7889
+ npx firegoat@latest login Log in to firegoat.ai: eval then uploads each session
7890
+ npx firegoat@latest logout Forget that login
7891
+ npx firegoat@latest feedback "<what got in the way>"
7892
+ Tell the firegoat team
7893
+
7894
+ Use npx firegoat@latest: plain npx firegoat can keep running an old cached version.
7895
+ Each eval prints its version first.
7836
7896
 
7837
7897
  Typical flow for a coding agent driving firegoat:
7838
- 1. Read npx firegoat init --help, then edit firegoat.yaml
7839
- 2. npx firegoat eval --json
7898
+ 1. Read npx firegoat@latest init --help, then edit firegoat.yaml
7899
+ 2. npx firegoat@latest eval --json
7840
7900
  3. Judge each run's end_goal from its run.json, session and workdir: firegoat only
7841
7901
  runs the script checks
7842
7902
 
@@ -7850,9 +7910,9 @@ In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs
7850
7910
  firegoat-onboarding skill globally, into ~/.claude/skills/ (npx skills add
7851
7911
  firegoat-ai/skills -g; in a terminal it asks: global, this repo, or none), offers
7852
7912
  firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
7853
- paste into your coding agent. Commit .firegoat/ with firegoat.yaml: its .gitignore
7854
- ignores only runs/ and .claude/, and test fixtures (helper scripts, sample data)
7855
- go in .firegoat/fixtures/. Its README.md says what each folder is.
7913
+ paste into your coding agent. .firegoat/ holds local run output and ignores itself
7914
+ in git. Keep test fixtures (helper scripts, sample data) outside it, e.g. in
7915
+ fixtures/ next to firegoat.yaml, and commit them.
7856
7916
 
7857
7917
  --force Replace an existing firegoat.yaml
7858
7918
  --local-skills Install the skill into this repo's .claude/skills/ instead
@@ -7868,26 +7928,41 @@ firegoat.yaml reference:
7868
7928
  --model, claude's default. eval --model overrides
7869
7929
  build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
7870
7930
  $FIREGOAT_OUT is first on the agent's PATH
7871
- install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts
7931
+ install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
7932
+ failure: that run is not graded
7933
+ cleanup: <sh> | [<sh>, ...] In each run's folder once the run is over: after the
7934
+ checks, and also after an error or timeout. Stops
7935
+ what the agent left running (servers, daemons).
7936
+ Every step runs; a failure is a warning (run.json
7937
+ cleanup, <test>/<n>/cleanup.txt), never the verdict
7872
7938
  vars: {KEY: value} Defaults for {{vars.KEY}} in prompts and checks;
7873
7939
  --var KEY=value overrides
7874
- env: {NAME: value} Env vars for the agent, and for install and
7875
- checks of the same run; values are strings and can
7940
+ env: {NAME: value} Env vars for the agent, and for install, checks
7941
+ and cleanup of the same run; values are strings and can
7876
7942
  use {{vars.KEY}}, {{run.id}}, {{run.n}},
7877
- {{test.name}}, $FIREGOAT_OUT, $FIREGOAT_WORKDIR
7878
- (no other $ expansion: $HOME stays literal).
7879
- The build step gets only the entries that use only
7880
- {{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
7943
+ {{test.name}}, $FIREGOAT_OUT, $FIREGOAT_WORKDIR,
7944
+ $FIREGOAT_HOME (no other $ expansion: $HOME stays
7945
+ literal). The build step gets only the entries that
7946
+ use only {{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
7881
7947
  PATH, CLAUDE_CONFIG_DIR, FIREGOAT_* and
7882
- ANTHROPIC_MODEL (use model:) are reserved.
7883
- Runs go 4 at a time: give a tool with a global home
7884
- a per-run one (or use eval --parallel 1), e.g.
7885
- MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{test.name}}-{{run.n}}"
7886
- Setting HOME: on macOS claude reads its login from
7887
- $HOME/Library, so a per-run HOME needs
7888
- ln -s <real home>/Library "$HOME/Library" in
7889
- install (get the real home in build, which
7890
- doesn't get entries using {{test.name}}/{{run.n}}).
7948
+ ANTHROPIC_MODEL (use model:) are reserved, and so
7949
+ are HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME,
7950
+ XDG_DATA_HOME, XDG_STATE_HOME and CLAUDE_ENV_FILE:
7951
+ every run gets its own home ($FIREGOAT_HOME), so a
7952
+ tool that keeps state in ~ starts fresh in each
7953
+ run. Only a tool that ignores HOME needs its own
7954
+ var, e.g. MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
7955
+ claude_args: [<arg>, ...] Extra claude arguments, after firegoat's own flags
7956
+ and --model. Each entry is one argument, passed
7957
+ as-is (no shell splitting): [--chrome] (Claude in
7958
+ Chrome, for tests that need a browser),
7959
+ [--plugin-dir, ./plugins/x], --mcp-config, --agents,
7960
+ --append-system-prompt. Flags firegoat sets are
7961
+ errors, also as --flag=value: -p, --print,
7962
+ --output-format, --verbose, --setting-sources,
7963
+ --strict-mcp-config, --disable-slash-commands,
7964
+ --dangerously-skip-permissions, --model (use
7965
+ model:), --session-id, --resume, -r, --continue, -c
7891
7966
  tests:
7892
7967
  - name: quickstart Letters, digits, '.', '_', '-'; unique
7893
7968
  prompt: ... What a user would ask the agent; one concrete outcome
@@ -7897,11 +7972,22 @@ firegoat.yaml reference:
7897
7972
  - run: ... Afterwards; exit 0 = pass; they set eval's exit code
7898
7973
  timeout: 600 Optional, seconds
7899
7974
  env: {NAME: value} Optional, on top of the top-level env:
7975
+ install: <sh> | [...] Optional, runs after the top-level install
7976
+ cleanup: <sh> | [...] Optional, runs before the top-level cleanup
7977
+ claude_args: [...] Optional, after the top-level claude_args
7978
+
7979
+ Step order: build once per eval; then per run: install (top-level, then the
7980
+ test's), the agent, checks, cleanup (the test's, then top-level).
7900
7981
 
7901
7982
  Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
7902
- .firegoat/fixtures/ and copy them in build or install. The build and install
7903
- commands and the checks get the vars as env vars, plus $FIREGOAT_OUT and
7904
- $FIREGOAT_WORKDIR. Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
7983
+ e.g. fixtures/ (committed) and copy them in build or install. To start each run
7984
+ from a template project, commit it as e.g. fixtures/starter-app/, then:
7985
+ build: cp -R fixtures/starter-app "$FIREGOAT_OUT/starter-app"
7986
+ install: cp -R "$FIREGOAT_OUT/starter-app/." .
7987
+ The build, install and cleanup commands and the checks get the vars as env
7988
+ vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
7989
+ get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME).
7990
+ Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
7905
7991
  transcript, greppable line by line: "[1] $ <command>" on every line of a shell
7906
7992
  call (one line per line of the command, same n), "[2] Write <path>" for other
7907
7993
  tools, each output indented, and "agent: " on every line of the agent's text and
@@ -7917,6 +8003,7 @@ firegoat.yaml reference:
7917
8003
  Example:
7918
8004
  build: npm pack --pack-destination "$FIREGOAT_OUT"
7919
8005
  install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
8006
+ cleanup: pkill -f "$FIREGOAT_WORKDIR" || true
7920
8007
  tests:
7921
8008
  - name: first-call
7922
8009
  prompt: >-
@@ -7930,6 +8017,12 @@ Example:
7930
8017
  - run: |
7931
8018
  test -s "$FIREGOAT_TRANSCRIPT" &&
7932
8019
  ! grep -Eq '^\\[[0-9]+\\] \\$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
8020
+ - name: dashboard-key
8021
+ prompt: >-
8022
+ Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put
8023
+ it in .env as ACME_API_KEY.
8024
+ end_goal: .env holds a key copied from the dashboard, not a made-up one.
8025
+ claude_args: [--chrome]
7933
8026
 
7934
8027
  Exit codes: 0 done, 1 the firegoat login failed (the files stay), 2 the skill
7935
8028
  install failed, 3 usage error.`;
@@ -7973,11 +8066,38 @@ Runs each test in firegoat.yaml (in the current folder): your claude gets the pr
7973
8066
  in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
7974
8067
  outputs on PATH. Then the test's checks run in that folder.
7975
8068
 
8069
+ Step order: build: once, in this folder; then per run, in its folder: install:
8070
+ (top-level, then the test's), the agent, checks, cleanup: (the test's, then
8071
+ top-level). cleanup: runs whenever the folder exists, also after a failed install,
8072
+ a claude error or a timeout; a failing cleanup step prints a warning and never
8073
+ changes the run's status or the exit code.
8074
+
7976
8075
  claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
7977
8076
  with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
7978
- plugins, skills or MCP servers. claude saves its session there. There is no
8077
+ plugins, skills or MCP servers (claude_args: in firegoat.yaml adds claude flags,
8078
+ e.g. --chrome or --plugin-dir). claude saves its session there. There is no
7979
8079
  sandbox: the agent can reach the rest of your machine, including the repo.
7980
8080
 
8081
+ Every run gets its own HOME, .firegoat/runs/<runId>/<test>/<n>/home (with
8082
+ XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
8083
+ $FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
8084
+ collide. claude itself keeps your real HOME (its login is there) and sources
8085
+ <n>/home.env (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
8086
+ checks and cleanup get the run's HOME directly; build keeps yours. Limits: claude's
8087
+ own Read/Write tools expand ~ to your real home; getpwuid lookups (os.userInfo,
8088
+ python pwd) still see it; keychain logins (gh, docker, git credential helpers) look
8089
+ logged out inside a run, so log tools in from install: or with env: tokens; the
8090
+ fresh home has no .gitconfig, so set user.name/user.email in install: if the agent
8091
+ commits; claude's shell snapshot still sources your real ~/.zshrc.
8092
+
8093
+ Vars a parent Claude Code session sets (CLAUDECODE, CLAUDE_CODE_*, CLAUDE_PID,
8094
+ CLAUDE_ENV_FILE, CLAUDE_SESSION_ID, CLAUDE_PROJECT_DIR, CLAUDE_EFFORT,
8095
+ CLAUDE_AGENT_SDK_*, CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_SKILL_DIR) are
8096
+ removed from every env firegoat starts, so the claude under test doesn't act as a
8097
+ nested session. Login and provider vars stay: ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOKEN,
8098
+ CLAUDE_CODE_OAUTH_REFRESH_TOKEN, CLAUDE_CODE_USE_*, CLAUDE_CODE_SKIP_*_AUTH,
8099
+ CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, CLAUDE_CODE_CLIENT_KEY_PASSPHRASE.
8100
+
7981
8101
  In a linked git worktree (git worktree add), .firegoat/runs/ and .firegoat/.claude
7982
8102
  live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
7983
8103
  fixtures and build: stay in the current folder.
@@ -7987,9 +8107,10 @@ fixtures and build: stay in the current folder.
7987
8107
  --model NAME claude --model, replacing model: in firegoat.yaml; repeat it
7988
8108
  (--model a --model b) to run each test runs times per model
7989
8109
  --parallel N Runs at once (default 4; 1 runs them one after another).
7990
- Tools with global on-disk state collide: give each run its
7991
- 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 checks
8110
+ Each run has its own HOME; state outside it (a fixed port,
8111
+ /tmp paths) can still collide: use 1 then
8112
+ --var KEY=value Fills {{vars.KEY}}; env for build, install, checks and
8113
+ cleanup
7993
8114
  --json The summary as JSON on stdout (or {error, exitCode} if eval
7994
8115
  stops early); progress goes to stderr
7995
8116
  --no-upload Keep runs local even when logged in to firegoat.ai
@@ -8015,17 +8136,21 @@ Output: .firegoat/runs/<runId>/
8015
8136
  uploadFailed
8016
8137
  <test>/<n>/run.json One run: status and reason (from the checks), model
8017
8138
  (asked for, or null) and claudeModel (what claude's
8018
- init line reported), prompt, end_goal,
8019
- checks [{run, code}], and paths: workdir,
8139
+ init line reported), claudeArgs (claude_args
8140
+ passed), prompt, end_goal,
8141
+ checks [{run, code}], cleanup [{run, code}],
8142
+ and paths: workdir, home (the run's HOME),
8020
8143
  transcript, session (claude's session file), stderr,
8021
- checksLog; startedAt; upload ({status: "uploaded",
8144
+ checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
8022
8145
  url} | {status: "failed", error} | {status:
8023
8146
  "skipped", reason}, null when not uploading)
8024
8147
  <test>/<n>/transcript.txt What the agent did: each tool call with its command
8025
8148
  ("[n] $ " on every line of a shell call), short output
8026
8149
  (indented), its text and final answer ("agent: " on
8027
8150
  every line), files it made
8028
- build.txt, <test>/<n>/install.txt, checks.txt Logs
8151
+ <test>/<n>/home/, home.env The run's HOME, and the file claude sources
8152
+ before the agent's Bash commands
8153
+ build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
8029
8154
 
8030
8155
  Grading: firegoat runs only the script checks; they set the exit code. end_goal is
8031
8156
  yours to judge: read transcript.txt first (a few KB); the session file has everything
@@ -8063,6 +8188,30 @@ Example:
8063
8188
 
8064
8189
  Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
8065
8190
 
8191
+ // src/home.ts
8192
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
8193
+ import { join as join3 } from "node:path";
8194
+ function homeEnv(home) {
8195
+ return {
8196
+ HOME: home,
8197
+ XDG_CONFIG_HOME: join3(home, ".config"),
8198
+ XDG_CACHE_HOME: join3(home, ".cache"),
8199
+ XDG_DATA_HOME: join3(home, ".local", "share"),
8200
+ XDG_STATE_HOME: join3(home, ".local", "state")
8201
+ };
8202
+ }
8203
+ var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
8204
+ function makeRunHome(dir) {
8205
+ const home = join3(dir, "home");
8206
+ const env = homeEnv(home);
8207
+ for (const d of Object.values(env))
8208
+ mkdirSync3(d, { recursive: true });
8209
+ const envFile = join3(dir, "home.env");
8210
+ writeFileSync2(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
8211
+ `).join(""));
8212
+ return { home, envFile };
8213
+ }
8214
+
8066
8215
  // src/steps.ts
8067
8216
  import { createWriteStream } from "node:fs";
8068
8217
  var STEP_TIMEOUT_MS = 600000;
@@ -8085,10 +8234,27 @@ async function runSteps(opts) {
8085
8234
  await new Promise((r) => log.end(r));
8086
8235
  }
8087
8236
  }
8237
+ async function runAllSteps(opts) {
8238
+ const log = createWriteStream(opts.logPath);
8239
+ const outcomes = [];
8240
+ try {
8241
+ for (const run of opts.commands)
8242
+ outcomes.push(await runShell({
8243
+ run,
8244
+ cwd: opts.cwd,
8245
+ env: opts.env,
8246
+ timeoutMs: STEP_TIMEOUT_MS,
8247
+ log
8248
+ }));
8249
+ return outcomes;
8250
+ } finally {
8251
+ await new Promise((r) => log.end(r));
8252
+ }
8253
+ }
8088
8254
 
8089
8255
  // src/transcript.ts
8090
- import { readdirSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
8091
- import { join as join3, relative } from "node:path";
8256
+ import { readdirSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
8257
+ import { join as join4, relative } from "node:path";
8092
8258
  var OUTPUT_LINES = 15;
8093
8259
  var OUTPUT_CHARS = 1500;
8094
8260
  var TEXT_CHARS = 1500;
@@ -8132,7 +8298,7 @@ function workdirFiles(dir) {
8132
8298
  for (const name of readdirSync(d)) {
8133
8299
  if (SKIP_DIRS.has(name))
8134
8300
  continue;
8135
- const p = join3(d, name);
8301
+ const p = join4(d, name);
8136
8302
  if (statSync(p).isDirectory())
8137
8303
  walk(p);
8138
8304
  else
@@ -8175,7 +8341,7 @@ ${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
8175
8341
  out.pop();
8176
8342
  out.push("", "## final answer", final ? prose(final) : "(none)");
8177
8343
  out.push("", "## files in the workdir", ...workdirFiles(opts.workdir));
8178
- writeFileSync2(opts.path, `${out.join(`
8344
+ writeFileSync3(opts.path, `${out.join(`
8179
8345
  `)}
8180
8346
  `);
8181
8347
  }
@@ -8355,7 +8521,7 @@ function ttyUI(out, opts) {
8355
8521
  const doneCount = c.pass + c.fail + c.error;
8356
8522
  const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
8357
8523
  const out = [
8358
- ...logoHeader(p, `firegoat eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
8524
+ ...logoHeader(p, `firegoat ${opts.version} eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
8359
8525
  rule
8360
8526
  ];
8361
8527
  if (authText)
@@ -8459,7 +8625,7 @@ function ttyUI(out, opts) {
8459
8625
  }
8460
8626
 
8461
8627
  // src/upload.ts
8462
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8628
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
8463
8629
  import { basename as basename2 } from "node:path";
8464
8630
  var MAX_FILE = 20 * 1024 * 1024;
8465
8631
  var MAX_CHECKS = 200;
@@ -8535,7 +8701,7 @@ async function uploadRun(t, r) {
8535
8701
  };
8536
8702
  const bytes = {};
8537
8703
  for (const name of FILES) {
8538
- const b = existsSync3(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
8704
+ const b = existsSync2(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
8539
8705
  if (b.byteLength > MAX_FILE)
8540
8706
  return {
8541
8707
  status: "failed",
@@ -8616,7 +8782,7 @@ async function uploadRun(t, r) {
8616
8782
 
8617
8783
  // src/which.ts
8618
8784
  import { accessSync, constants, statSync as statSync2 } from "node:fs";
8619
- import { delimiter, join as join4, resolve } from "node:path";
8785
+ import { delimiter, join as join5, resolve } from "node:path";
8620
8786
  function resolveBin(bin) {
8621
8787
  const executable = (p) => {
8622
8788
  try {
@@ -8629,8 +8795,8 @@ function resolveBin(bin) {
8629
8795
  if (bin.includes("/"))
8630
8796
  return executable(bin) ? resolve(bin) : undefined;
8631
8797
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
8632
- if (dir && executable(join4(dir, bin)))
8633
- return join4(dir, bin);
8798
+ if (dir && executable(join5(dir, bin)))
8799
+ return join5(dir, bin);
8634
8800
  }
8635
8801
  return;
8636
8802
  }
@@ -8703,12 +8869,12 @@ function parseArgs(args) {
8703
8869
  return opts;
8704
8870
  }
8705
8871
  function findSession(configDir, sessionId) {
8706
- const projects = join5(configDir, "projects");
8707
- if (!existsSync4(projects))
8872
+ const projects = join6(configDir, "projects");
8873
+ if (!existsSync3(projects))
8708
8874
  return null;
8709
8875
  for (const d of readdirSync2(projects)) {
8710
- const f = join5(projects, d, `${sessionId}.jsonl`);
8711
- if (existsSync4(f))
8876
+ const f = join6(projects, d, `${sessionId}.jsonl`);
8877
+ if (existsSync3(f))
8712
8878
  return f;
8713
8879
  }
8714
8880
  return null;
@@ -8718,7 +8884,7 @@ function newRunId() {
8718
8884
  return `${ts}-${randomBytes2(2).toString("hex")}`;
8719
8885
  }
8720
8886
  function writeRunJson(r) {
8721
- writeFileSync3(join5(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
8887
+ writeFileSync4(join6(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
8722
8888
  `);
8723
8889
  }
8724
8890
  async function uploadPreflight(opts, ui, project, runGroup, git) {
@@ -8760,14 +8926,16 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
8760
8926
  async function runOne(opts) {
8761
8927
  const { test, n } = opts;
8762
8928
  const started = Date.now();
8763
- const dir = join5(opts.runDir, test.name, String(n));
8764
- const workdir = join5(dir, "work");
8765
- mkdirSync3(workdir, { recursive: true });
8929
+ const dir = join6(opts.runDir, test.name, String(n));
8930
+ const workdir = join6(dir, "work");
8931
+ mkdirSync4(workdir, { recursive: true });
8932
+ const { home, envFile } = makeRunHome(dir);
8766
8933
  const r = {
8767
8934
  test: test.name,
8768
8935
  n,
8769
8936
  model: opts.model,
8770
8937
  claudeModel: null,
8938
+ claudeArgs: test.claudeArgs,
8771
8939
  status: "error",
8772
8940
  reason: "",
8773
8941
  prompt: render(test.prompt, opts.vars),
@@ -8775,51 +8943,77 @@ async function runOne(opts) {
8775
8943
  checks: [],
8776
8944
  dir,
8777
8945
  workdir,
8946
+ home,
8778
8947
  session: null,
8779
- transcript: join5(dir, "transcript.txt"),
8780
- stderr: join5(dir, "stderr.txt"),
8781
- checksLog: join5(dir, "checks.txt"),
8948
+ transcript: join6(dir, "transcript.txt"),
8949
+ stderr: join6(dir, "stderr.txt"),
8950
+ checksLog: join6(dir, "checks.txt"),
8951
+ cleanup: [],
8952
+ cleanupLog: join6(dir, "cleanup.txt"),
8782
8953
  num_turns: null,
8783
8954
  cost: null,
8784
8955
  duration_ms: 0,
8785
8956
  startedAt: new Date(started).toISOString(),
8786
8957
  upload: null
8787
8958
  };
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
8959
  const env = renderEnv(test.env, opts.vars, {
8796
8960
  "run.id": opts.runId,
8797
8961
  "run.n": String(n),
8798
8962
  "test.name": test.name,
8799
8963
  FIREGOAT_OUT: opts.out,
8800
- FIREGOAT_WORKDIR: workdir
8964
+ FIREGOAT_WORKDIR: workdir,
8965
+ FIREGOAT_HOME: home
8801
8966
  });
8802
- const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
8803
- if (opts.install.length > 0) {
8967
+ const stepEnv = {
8968
+ ...opts.stepEnv,
8969
+ ...env,
8970
+ ...homeEnv(home),
8971
+ FIREGOAT_WORKDIR: workdir,
8972
+ FIREGOAT_HOME: home
8973
+ };
8974
+ const [status, reason] = await attempt(opts, r, env, stepEnv, envFile);
8975
+ const cleanup = [...test.cleanup, ...opts.cleanup];
8976
+ if (cleanup.length > 0)
8977
+ r.cleanup = await runAllSteps({
8978
+ commands: cleanup,
8979
+ cwd: workdir,
8980
+ env: stepEnv,
8981
+ logPath: r.cleanupLog
8982
+ });
8983
+ r.status = status;
8984
+ r.reason = reason;
8985
+ r.duration_ms = Date.now() - started;
8986
+ writeRunJson(r);
8987
+ return r;
8988
+ }
8989
+ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
8990
+ const { test, n } = opts;
8991
+ const { dir, workdir } = r;
8992
+ const install = [...opts.install, ...test.install];
8993
+ if (install.length > 0) {
8804
8994
  const error = await runSteps({
8805
8995
  what: "install",
8806
- commands: opts.install,
8996
+ commands: install,
8807
8997
  cwd: workdir,
8808
8998
  env: stepEnv,
8809
- logPath: join5(dir, "install.txt")
8999
+ logPath: join6(dir, "install.txt")
8810
9000
  });
8811
9001
  if (error)
8812
- return done("error", `not graded: ${error}`);
9002
+ return ["error", `not graded: ${error}`];
8813
9003
  }
8814
9004
  const stderr = createWriteStream2(r.stderr);
8815
9005
  const outcome = await runClaude({
8816
9006
  bin: opts.claudeBin,
8817
9007
  prompt: r.prompt,
8818
9008
  cwd: workdir,
8819
- env: { ...claudeEnv(opts.configDir, env), PATH: opts.stepEnv.PATH },
9009
+ env: {
9010
+ ...claudeEnv(opts.configDir, env, homeEnvFile),
9011
+ PATH: opts.stepEnv.PATH
9012
+ },
8820
9013
  stderr,
8821
9014
  timeoutMs: test.timeout * 1000,
8822
9015
  model: opts.model ?? undefined,
9016
+ extraArgs: r.claudeArgs,
8823
9017
  onEvent: opts.onEvent
8824
9018
  });
8825
9019
  r.claudeModel = outcome.claudeModel;
@@ -8836,9 +9030,12 @@ async function runOne(opts) {
8836
9030
  r.num_turns = outcome.result?.num_turns ?? null;
8837
9031
  r.cost = outcome.result?.total_cost_usd ?? null;
8838
9032
  if (!outcome.ok)
8839
- return done("error", `not graded: ${outcome.reason}`);
9033
+ return ["error", `not graded: ${outcome.reason}`];
8840
9034
  if (!r.session)
8841
- return done("error", `not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`);
9035
+ return [
9036
+ "error",
9037
+ `not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`
9038
+ ];
8842
9039
  const log = createWriteStream2(r.checksLog);
8843
9040
  try {
8844
9041
  for (const c of test.checks)
@@ -8858,8 +9055,14 @@ async function runOne(opts) {
8858
9055
  }
8859
9056
  const failed = r.checks.filter((c) => c.code !== 0);
8860
9057
  if (failed.length > 0)
8861
- return done("fail", failed.map((c) => `check failed (${c.error ?? `exit ${c.code}`}): ${c.run}`).join("; "));
8862
- return done("pass", test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`);
9058
+ return [
9059
+ "fail",
9060
+ failed.map((c) => `check failed (${c.error ?? `exit ${c.code}`}): ${c.run}`).join("; ")
9061
+ ];
9062
+ return [
9063
+ "pass",
9064
+ test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
9065
+ ];
8863
9066
  }
8864
9067
  async function evalCommand(args, root) {
8865
9068
  if (args.includes("--help") || args.includes("-h")) {
@@ -8883,7 +9086,13 @@ async function evalCommand(args, root) {
8883
9086
  }
8884
9087
  const say = opts.json ? console.error : console.log;
8885
9088
  const stream = opts.json ? process.stderr : process.stdout;
8886
- const ui = stream.isTTY ? ttyUI(stream, { project: basename3(root), color: !process.env.NO_COLOR }) : plainUI(say);
9089
+ const ui = stream.isTTY ? ttyUI(stream, {
9090
+ project: basename3(root),
9091
+ version: package_default.version,
9092
+ color: !process.env.NO_COLOR
9093
+ }) : plainUI(say);
9094
+ if (!stream.isTTY)
9095
+ say(`firegoat ${package_default.version}`);
8887
9096
  let config;
8888
9097
  try {
8889
9098
  config = loadConfig(root);
@@ -8922,8 +9131,8 @@ async function evalCommand(args, root) {
8922
9131
  return fatal(EXIT.usage, e.message);
8923
9132
  }
8924
9133
  const firegoatDir = ensureFiregoatDir(repo?.mainCheckout ?? root);
8925
- const configDir = join5(firegoatDir, ".claude");
8926
- mkdirSync3(configDir, { recursive: true });
9134
+ const configDir = join6(firegoatDir, ".claude");
9135
+ mkdirSync4(configDir, { recursive: true });
8927
9136
  ui.auth(process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login");
8928
9137
  const loginProblem = checkLogin({
8929
9138
  bin: claudePath,
@@ -8945,10 +9154,10 @@ async function evalCommand(args, root) {
8945
9154
  return fatal(EXIT.usage, e.message);
8946
9155
  throw e;
8947
9156
  }
8948
- const runsRoot = join5(firegoatDir, "runs");
8949
- const runDir = join5(runsRoot, runId);
8950
- const out = join5(runDir, "out");
8951
- mkdirSync3(out, { recursive: true });
9157
+ const runsRoot = join6(firegoatDir, "runs");
9158
+ const runDir = join6(runsRoot, runId);
9159
+ const out = join6(runDir, "out");
9160
+ mkdirSync4(out, { recursive: true });
8952
9161
  const stepEnv = {
8953
9162
  ...hostEnv(),
8954
9163
  ...vars,
@@ -8974,7 +9183,7 @@ async function evalCommand(args, root) {
8974
9183
  ...stepEnv,
8975
9184
  ...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
8976
9185
  },
8977
- logPath: join5(runDir, "build.txt")
9186
+ logPath: join6(runDir, "build.txt")
8978
9187
  });
8979
9188
  const secs = Math.round((Date.now() - buildStarted) / 1000);
8980
9189
  ui.build(error ? "fail" : "ok", secs);
@@ -9011,6 +9220,7 @@ async function evalCommand(args, root) {
9011
9220
  claudeBin: claudePath,
9012
9221
  configDir,
9013
9222
  install: config.install,
9223
+ cleanup: config.cleanup,
9014
9224
  out,
9015
9225
  stepEnv,
9016
9226
  vars
@@ -9046,6 +9256,7 @@ async function evalCommand(args, root) {
9046
9256
  totals[r.status]++;
9047
9257
  const uploadsFailed = results.filter((r) => r.upload?.status === "failed").length;
9048
9258
  const doc = {
9259
+ firegoatVersion: package_default.version,
9049
9260
  runId,
9050
9261
  runsDir: runDir,
9051
9262
  dir: root,
@@ -9054,9 +9265,9 @@ async function evalCommand(args, root) {
9054
9265
  totals,
9055
9266
  runs: results
9056
9267
  };
9057
- writeFileSync3(join5(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
9268
+ writeFileSync4(join6(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
9058
9269
  `);
9059
- appendFileSync(join5(runsRoot, "index.jsonl"), `${JSON.stringify({
9270
+ appendFileSync(join6(runsRoot, "index.jsonl"), `${JSON.stringify({
9060
9271
  runId,
9061
9272
  startedAt: new Date(evalStarted).toISOString(),
9062
9273
  dir: root,
@@ -9076,6 +9287,10 @@ async function evalCommand(args, root) {
9076
9287
  rows,
9077
9288
  seconds: Math.round((Date.now() - evalStarted) / 1000)
9078
9289
  });
9290
+ for (const r of results)
9291
+ for (const c of r.cleanup)
9292
+ if (c.code !== 0)
9293
+ console.error(`firegoat eval: Warning: ${r.test}#${r.n} cleanup failed (${c.error ?? `exit ${c.code}`}): ${c.run} (log: ${r.cleanupLog})`);
9079
9294
  if (uploadsFailed > 0)
9080
9295
  console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
9081
9296
  if (opts.json)
@@ -9352,8 +9567,8 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
9352
9567
 
9353
9568
  // src/init.ts
9354
9569
  import { spawnSync as spawnSync3 } from "node:child_process";
9355
- import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "node:fs";
9356
- import { basename as basename4, join as join6 } from "node:path";
9570
+ import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
9571
+ import { basename as basename4, join as join7 } from "node:path";
9357
9572
  import { createInterface } from "node:readline/promises";
9358
9573
 
9359
9574
  // src/login.ts
@@ -9548,7 +9763,7 @@ function skillsDir(scope) {
9548
9763
  if (scope === "local")
9549
9764
  return ".claude/skills/";
9550
9765
  const config = process.env.CLAUDE_CONFIG_DIR?.trim();
9551
- return config ? `${join6(config, "skills")}/` : "~/.claude/skills/";
9766
+ return config ? `${join7(config, "skills")}/` : "~/.claude/skills/";
9552
9767
  }
9553
9768
  function template(product) {
9554
9769
  return `# firegoat.yaml: evals for how well coding agents use ${product}.
@@ -9579,34 +9794,48 @@ runs: 1
9579
9794
  # (An npm/pypi CLI that users run via npx/uvx: pack + install it as above instead;
9580
9795
  # a binary on PATH does not stop "npx mycli" from fetching the registry release)
9581
9796
  #
9582
- # Helper files (scripts, sample data) go in .firegoat/fixtures/ (committed), not inline here.
9583
- # The build step runs in this folder, so: build: cp -R .firegoat/fixtures/. "$FIREGOAT_OUT"
9797
+ # Helper files (scripts, sample data) go in e.g. fixtures/ (committed), not inline here.
9798
+ # The build step runs in this folder, so: build: cp -R fixtures/. "$FIREGOAT_OUT"
9584
9799
  # Then e.g. install: cp "$FIREGOAT_OUT/wizard.mjs" .
9585
-
9586
- # Optional: env vars for the agent (and for install and checks of the same
9587
- # run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
9588
- # {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR (no
9589
- # other $ expansion: $HOME stays literal). A test can add its own env:.
9590
- # Runs go 4 at a time (--parallel), so give a tool with a global home (~/.mytool)
9591
- # a per-run one, as below, or use --parallel 1.
9800
+ #
9801
+ # To start each run from a template project, commit it as e.g. fixtures/starter-app/:
9802
+ # build: cp -R fixtures/starter-app "$FIREGOAT_OUT/starter-app"
9803
+ # install: cp -R "$FIREGOAT_OUT/starter-app/." .
9804
+
9805
+ # Optional: cleanup: runs in each run's folder once the run is over (after the
9806
+ # checks, also after an error or timeout), e.g. to stop a server the agent left
9807
+ # running. A failing cleanup step is a warning, never the verdict.
9808
+ # cleanup:
9809
+ # - pkill -f "$FIREGOAT_WORKDIR" || true
9810
+ #
9811
+ # A test can add its own install: (after this one) and cleanup: (before this one).
9812
+
9813
+ # Optional: env vars for the agent (and for install, checks and cleanup of the
9814
+ # same run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
9815
+ # {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR /
9816
+ # $FIREGOAT_HOME (no other $ expansion: $HOME stays literal). A test can add its
9817
+ # own env:. Every run gets its own HOME ($FIREGOAT_HOME), so tools that keep state
9818
+ # in ~ are isolated per run already; set a tool's own *_HOME var only if the tool
9819
+ # ignores HOME, as below.
9592
9820
  # env:
9593
- # MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{test.name}}-{{run.n}}"
9821
+ # MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
9594
9822
  # UV_FIND_LINKS: "$FIREGOAT_OUT"
9595
9823
 
9596
9824
  tests:
9597
- - name: quickstart
9598
- # What a user would ask their agent. Edit this.
9599
- prompt: >-
9600
- I want to try ${product}. Get the smallest working example going in a new
9601
- project, run it, and save what it prints to output.txt. Keep it minimal.
9825
+ # A smoke test that firegoat and claude work. Replace it with tests for your
9826
+ # product: the firegoat-onboarding skill writes them.
9827
+ - name: goat
9828
+ # What a user would ask their agent.
9829
+ prompt: Who's the GOAT? Answer in one short sentence.
9602
9830
  # True when the user's problem is solved. Not graded by firegoat: it goes to
9603
9831
  # your agent (eval --json, run.json) to judge.
9604
- end_goal: >-
9605
- A minimal project uses ${product}, runs without errors, and output.txt holds
9606
- its output.
9832
+ end_goal: Claude answers that you (the user) are the goat, e.g. YOU'RE THE GOAT.
9607
9833
  # Shell commands run in the agent's folder afterwards; exit 0 = pass.
9834
+ # $FIREGOAT_TRANSCRIPT has "agent: " on every line of the agent's text.
9608
9835
  checks:
9609
- - run: test -s output.txt
9836
+ - run: >-
9837
+ test -s "$FIREGOAT_TRANSCRIPT" &&
9838
+ grep -Eiq '^agent: .*goat' "$FIREGOAT_TRANSCRIPT"
9610
9839
  # Seconds before the agent run is stopped.
9611
9840
  timeout: 600
9612
9841
  `;
@@ -9725,16 +9954,16 @@ ${INIT_HELP}`);
9725
9954
  console.log(rule);
9726
9955
  }
9727
9956
  ensureFiregoatDir(cwd);
9728
- const configPath = join6(cwd, CONFIG_FILE);
9729
- const keep = existsSync5(configPath) && !force;
9957
+ const configPath = join7(cwd, CONFIG_FILE);
9958
+ const keep = existsSync4(configPath) && !force;
9730
9959
  if (!keep)
9731
- writeFileSync4(configPath, template(basename4(cwd)));
9960
+ writeFileSync5(configPath, template(basename4(cwd)));
9732
9961
  if (interactive) {
9733
9962
  step(1, CONFIG_FILE, keep ? "Kept (--force replaces it)" : "Created");
9734
9963
  console.log(paint("90", " │"));
9735
9964
  step(2, ".firegoat/", "Ready");
9736
9965
  } else
9737
- console.log(keep ? `Kept ${CONFIG_FILE} (--force replaces it)` : `Wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
9966
+ console.log(keep ? `Kept ${CONFIG_FILE} (--force replaces it)` : `Wrote ${CONFIG_FILE}: replace the goat smoke test with tests for your product`);
9738
9967
  if (interactive && !noSkills)
9739
9968
  console.log(paint("90", " │"));
9740
9969
  if (interactive && !noSkills && !localSkills) {
@@ -9788,7 +10017,7 @@ Prompt for your coding agent: ${AGENT_PROMPT}`);
9788
10017
  console.log(AGENT_PROMPT);
9789
10018
  console.log("");
9790
10019
  const copied = copyToClipboard(AGENT_PROMPT);
9791
- console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}Then: ${paint("36", "npx firegoat eval")}`);
10020
+ console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}Then: ${paint("36", "npx firegoat@latest eval")}`);
9792
10021
  return EXIT.pass;
9793
10022
  }
9794
10023
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/firegoat-ai/firegoat-cli.git"