firegoat 0.25.0 → 0.26.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 +2 -0
  2. package/dist/cli.js +153 -76
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -37,6 +37,8 @@ tests:
37
37
  - run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
38
38
  ```
39
39
 
40
+ Each run starts in a fresh, empty folder outside your repo, `~/.firegoat/runs/<hash>/<random>/work`, so its path doesn't name your project and the agent doesn't wander into your source. Logs and results stay in the repo, in `.firegoat/runs/`.
41
+
40
42
  ## Uploads
41
43
 
42
44
  Log in to your firegoat.ai account and each trace is uploaded to our system automatically. Secrets are masked in everything you see, but the uploaded files are stored as sent. You can opt out of uploading with `eval --no-upload`. You can also delete your traces on the dashboard, and the files and the record are gone from our site.
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.25.0",
6950
+ version: "0.26.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
@@ -6976,7 +6976,7 @@ var package_default = {
6976
6976
  },
6977
6977
  scripts: {
6978
6978
  build: "bun build src/cli.ts --target node --outfile dist/cli.js",
6979
- test: "bun test",
6979
+ test: "bun test --timeout 30000",
6980
6980
  typecheck: "bun check",
6981
6981
  lint: "biome check .",
6982
6982
  format: "biome check --write .",
@@ -8150,10 +8150,54 @@ import { randomBytes as randomBytes2 } from "node:crypto";
8150
8150
  import {
8151
8151
  appendFileSync,
8152
8152
  createWriteStream as createWriteStream2,
8153
- mkdirSync as mkdirSync4,
8153
+ mkdirSync as mkdirSync5,
8154
8154
  writeFileSync as writeFileSync4
8155
8155
  } from "node:fs";
8156
- import { basename as basename3, delimiter as delimiter2, join as join9 } from "node:path";
8156
+ import { basename as basename3, delimiter as delimiter2, join as join10 } from "node:path";
8157
+
8158
+ // src/agent-dirs.ts
8159
+ import { createHash, randomInt } from "node:crypto";
8160
+ import { mkdirSync as mkdirSync2, realpathSync, symlinkSync as symlinkSync3 } from "node:fs";
8161
+ import { homedir as homedir3 } from "node:os";
8162
+ import { join as join5 } from "node:path";
8163
+ var ID_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
8164
+ var ID_LENGTH = 8;
8165
+ var MAX_TRIES = 10;
8166
+ function projectAgentDir(projectRoot) {
8167
+ const hash = createHash("sha256").update(realpathSync(projectRoot)).digest("hex").slice(0, 12);
8168
+ return join5(firegoatHome(), "runs", hash);
8169
+ }
8170
+ function firegoatHome() {
8171
+ return join5(realpathSync(homedir3()), ".firegoat");
8172
+ }
8173
+ function randomId() {
8174
+ let id = "";
8175
+ for (let i = 0;i < ID_LENGTH; i++)
8176
+ id += ID_CHARS[randomInt(ID_CHARS.length)];
8177
+ return id;
8178
+ }
8179
+ function ensureProjectAgentDir(projectRoot) {
8180
+ mkdirSync2(firegoatHome(), { recursive: true, mode: 448 });
8181
+ const dir = projectAgentDir(projectRoot);
8182
+ mkdirSync2(dir, { recursive: true });
8183
+ return dir;
8184
+ }
8185
+ function makeAgentDir(projectDir) {
8186
+ for (let i = 0;i < MAX_TRIES; i++) {
8187
+ const dir = join5(projectDir, randomId());
8188
+ try {
8189
+ mkdirSync2(dir);
8190
+ return dir;
8191
+ } catch (e) {
8192
+ if (e.code !== "EEXIST")
8193
+ throw e;
8194
+ }
8195
+ }
8196
+ throw new Error(`Could not create a new run folder in ${projectDir}: ${MAX_TRIES} random names were taken`);
8197
+ }
8198
+ function linkDir(target, link) {
8199
+ symlinkSync3(target, link, "dir");
8200
+ }
8157
8201
 
8158
8202
  // src/credentials.ts
8159
8203
  import { randomBytes } from "node:crypto";
@@ -8162,15 +8206,15 @@ import {
8162
8206
  closeSync as closeSync2,
8163
8207
  fsyncSync,
8164
8208
  lstatSync as lstatSync3,
8165
- mkdirSync as mkdirSync2,
8209
+ mkdirSync as mkdirSync3,
8166
8210
  openSync as openSync2,
8167
8211
  readFileSync as readFileSync4,
8168
8212
  renameSync,
8169
8213
  unlinkSync as unlinkSync3,
8170
8214
  writeSync
8171
8215
  } from "node:fs";
8172
- import { homedir as homedir3 } from "node:os";
8173
- import { join as join5 } from "node:path";
8216
+ import { homedir as homedir4 } from "node:os";
8217
+ import { join as join6 } from "node:path";
8174
8218
 
8175
8219
  // src/firegoat-api.ts
8176
8220
  var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
@@ -8322,10 +8366,10 @@ async function whoami(base, key) {
8322
8366
  // src/credentials.ts
8323
8367
  var KEY = /^fire_[\x21-\x7e]+$/;
8324
8368
  function credentialsDir() {
8325
- return join5(homedir3(), ".firegoat");
8369
+ return join6(homedir4(), ".firegoat");
8326
8370
  }
8327
8371
  function credentialsPath() {
8328
- return join5(credentialsDir(), "credentials.json");
8372
+ return join6(credentialsDir(), "credentials.json");
8329
8373
  }
8330
8374
  function uid() {
8331
8375
  if (!process.getuid)
@@ -8388,7 +8432,7 @@ function saveCredentials(c) {
8388
8432
  if (st)
8389
8433
  assertOwned(dir, st, "dir");
8390
8434
  else
8391
- mkdirSync2(dir, { mode: 448 });
8435
+ mkdirSync3(dir, { mode: 448 });
8392
8436
  chmodSync(dir, 448);
8393
8437
  const tmp = `${credentialsPath()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
8394
8438
  const fd = openSync2(tmp, "wx", 384);
@@ -8444,15 +8488,15 @@ function resolveKey(base) {
8444
8488
  var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
8445
8489
 
8446
8490
  // src/firegoat-dir.ts
8447
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
8448
- import { join as join6 } from "node:path";
8491
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
8492
+ import { join as join7 } from "node:path";
8449
8493
  var GITIGNORE = `*
8450
8494
  `;
8451
8495
  function ensureFiregoatDir(root) {
8452
- const dir = join6(root, ".firegoat");
8453
- mkdirSync3(dir, { recursive: true });
8496
+ const dir = join7(root, ".firegoat");
8497
+ mkdirSync4(dir, { recursive: true });
8454
8498
  try {
8455
- writeFileSync2(join6(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
8499
+ writeFileSync2(join7(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
8456
8500
  } catch (e) {
8457
8501
  if (e.code !== "EEXIST")
8458
8502
  throw e;
@@ -8812,7 +8856,11 @@ firegoat.yaml reference:
8812
8856
  for any value.
8813
8857
  The build, install and cleanup commands and the checks get the vars as env
8814
8858
  vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
8815
- get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME).
8859
+ get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME),
8860
+ $FIREGOAT_RUN_DIR (the run's .firegoat/runs/<runId>/<test>/<n>/, where its logs
8861
+ go) and $FIREGOAT_REPO (the folder holding firegoat.yaml). The agent gets
8862
+ neither: keep values it must not find in $FIREGOAT_RUN_DIR, e.g. install:
8863
+ echo "$EXPECTED" > "$FIREGOAT_RUN_DIR/expected", and a check reads it back.
8816
8864
  Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
8817
8865
  transcript, greppable line by line (format: eval --help, Transcript). So
8818
8866
  '^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
@@ -8911,8 +8959,13 @@ var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--pa
8911
8959
 
8912
8960
  Runs each test in firegoat.yaml (in the current folder): your agent harness
8913
8961
  (claude-code by default, codex-cli or grok-cli; harness: in firegoat.yaml) gets the prompt
8914
- in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
8915
- outputs on PATH. Then the test's checks run in that folder.
8962
+ in a fresh, empty folder outside the repo, ~/.firegoat/runs/<hash>/<random>/work,
8963
+ with build:'s outputs on PATH. Then the test's checks run in that folder. <hash>
8964
+ is a hash of the repo's path (a linked worktree uses its main checkout's) and
8965
+ <random> is new for each run, so the agent's paths name neither the project nor
8966
+ the test, and git there sees no repo. The run's logs stay in the repo, in
8967
+ .firegoat/runs/<runId>/<test>/<n>/, whose work and home are symlinks to the
8968
+ run's folders.
8916
8969
 
8917
8970
  Step order: build: once, in this folder; then per run, in its folder: install:
8918
8971
  (top-level, then the test's), the agent, checks, cleanup: (the test's, then
@@ -8921,38 +8974,39 @@ a claude error or a timeout; a failing cleanup step prints a warning and never
8921
8974
  changes the run's status or the exit code.
8922
8975
 
8923
8976
  claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
8924
- with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
8925
- plugins, skills or MCP servers (harness_args: in firegoat.yaml adds claude flags,
8926
- e.g. --chrome or --plugin-dir). claude saves its session there. There is no
8927
- sandbox: the agent can reach the rest of your machine, including the repo.
8977
+ with a fresh config dir, ~/.firegoat/runs/<hash>/.claude: none of your settings,
8978
+ CLAUDE.md, plugins, skills or MCP servers (harness_args: in firegoat.yaml adds
8979
+ claude flags, e.g. --chrome or --plugin-dir). claude saves its session there. There
8980
+ is no sandbox: the agent can still reach the rest of your machine, including the
8981
+ repo, if it goes looking.
8928
8982
 
8929
8983
  codex-cli runs codex exec --json --dangerously-bypass-approvals-and-sandbox with a
8930
- fresh CODEX_HOME, .firegoat/.codex, holding a link to your codex login
8984
+ fresh CODEX_HOME, ~/.firegoat/runs/<hash>/.codex, holding a link to your codex login
8931
8985
  (~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
8932
8986
  -c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
8933
8987
  yours. codex's own HOME is the run's home (below), also for its Read/Write. It
8934
- saves its session (rollout) under .firegoat/.codex/sessions/. firegoat.ai doesn't
8988
+ saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai doesn't
8935
8989
  read codex sessions yet: those runs are not uploaded. codex reports no turns or
8936
8990
  cost; its model comes from the session file.
8937
8991
 
8938
8992
  grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
8939
8993
  PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
8940
8994
  --output-format streaming-messages-json --always-approve --no-auto-update, with a
8941
- fresh GROK_HOME, .firegoat/.grok, holding a link to your grok login
8995
+ fresh GROK_HOME, ~/.firegoat/runs/<hash>/.grok, holding a link to your grok login
8942
8996
  ($GROK_HOME/auth.json or ~/.grok/auth.json; run grok login first; XAI_API_KEY alone
8943
8997
  does not log headless grok in). grok's own HOME is the run's home (below): with
8944
8998
  your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eval
8945
8999
  refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
8946
9000
  a refresh inside a run would replace the link with a file, which the next eval
8947
9001
  asks you to move back over ~/.grok/auth.json. The session is a directory,
8948
- .firegoat/.grok/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). firegoat.ai doesn't read
9002
+ $GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). firegoat.ai doesn't read
8949
9003
  grok sessions yet: those runs are not uploaded.
8950
9004
 
8951
- Every run gets its own HOME, .firegoat/runs/<runId>/<test>/<n>/home (with
8952
- XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
9005
+ Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
9006
+ (with XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
8953
9007
  $FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
8954
9008
  collide. claude itself keeps your real HOME (its login is there) and sources
8955
- <n>/home.env (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
9009
+ home.env next to it (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
8956
9010
  checks and cleanup get the run's HOME directly; build keeps yours. Limits: claude's
8957
9011
  own Read/Write tools expand ~ to your real home; getpwuid lookups (os.userInfo,
8958
9012
  python pwd) still see it; keychain logins (gh, docker, git credential helpers) look
@@ -8968,9 +9022,9 @@ nested session. Login and provider vars stay: ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOK
8968
9022
  CLAUDE_CODE_OAUTH_REFRESH_TOKEN, CLAUDE_CODE_USE_*, CLAUDE_CODE_SKIP_*_AUTH,
8969
9023
  CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, CLAUDE_CODE_CLIENT_KEY_PASSPHRASE.
8970
9024
 
8971
- In a linked git worktree (git worktree add), .firegoat/runs/ and the harness
8972
- config dirs (.firegoat/.claude, .firegoat/.codex, .firegoat/.grok) live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
8973
- fixtures and build: stay in the current folder.
9025
+ In a linked git worktree (git worktree add), .firegoat/runs/ lives in the main
9026
+ checkout instead, and <hash> is the main checkout's, so runs outlive the worktree;
9027
+ firegoat.yaml, fixtures and build: stay in the current folder.
8974
9028
 
8975
9029
  test.. Only these tests (by name)
8976
9030
  --runs N Runs per test (overrides runs: in firegoat.yaml)
@@ -8994,8 +9048,9 @@ fixtures and build: stay in the current folder.
8994
9048
  --model, --harness, missing vars), print each planned
8995
9049
  run ("<test>#<n> [harness model]" with its install, checks
8996
9050
  and cleanup counts and rendered harness_args, paths under
8997
- runs/<runId>/) and exit 0. Builds nothing, starts no
8998
- agent, uploads nothing, writes no files. Config errors
9051
+ ~/.firegoat/runs/<hash>/<random>/) and exit 0. Builds
9052
+ nothing, starts no agent, uploads nothing, writes no
9053
+ files. Config errors
8999
9054
  exit 3. With --json: {dryRun, build, runs: [{test, n,
9000
9055
  harness, model, harnessArgs, install, checks,
9001
9056
  cleanup}]}, steps and checks with their placeholders
@@ -9017,6 +9072,8 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
9017
9072
  (default https://firegoat.ai/evals). See firegoat login --help.
9018
9073
 
9019
9074
  Output: .firegoat/runs/<runId>/
9075
+ out A symlink to $FIREGOAT_OUT,
9076
+ ~/.firegoat/runs/<hash>/<random>/out
9020
9077
  summary.json firegoatVersion, runId, runsDir, dir (where eval
9021
9078
  ran), git {branch, commit, dirty} (no git outside a
9022
9079
  repo), harnesses, upload {origin, email} (null when
@@ -9030,14 +9087,17 @@ Output: .firegoat/runs/<runId>/
9030
9087
  the harness reported running, or null), harnessArgs
9031
9088
  (harness_args passed), prompt, end_goal,
9032
9089
  checks [{run, code}], cleanup [{run, code}],
9033
- and paths: workdir, home (the run's HOME),
9090
+ and paths: dir (this folder), workdir, home (the
9091
+ run's HOME; both real paths, under ~/.firegoat),
9034
9092
  transcript, session (the harness's session file), stderr,
9035
9093
  checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
9036
9094
  url} | {status: "failed", error} | {status:
9037
9095
  "skipped", reason}, null when not uploading)
9038
9096
  <test>/<n>/transcript.txt What the agent did, below
9039
- <test>/<n>/home/, home.env The run's HOME, and the file claude sources
9040
- before the agent's Bash commands
9097
+ <test>/<n>/work, home Symlinks to the agent's folder and the run's HOME
9098
+ (home.env, which claude sources before the agent's
9099
+ Bash commands, is next to them)
9100
+
9041
9101
  build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
9042
9102
 
9043
9103
  Transcript ($FIREGOAT_TRANSCRIPT), greppable line by line; n counts tool calls:
@@ -9228,7 +9288,7 @@ async function sendTelemetry(event, exitCode, startedAt) {
9228
9288
 
9229
9289
  // src/transcript.ts
9230
9290
  import { readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
9231
- import { join as join7, relative } from "node:path";
9291
+ import { join as join8, relative } from "node:path";
9232
9292
  var OUTPUT_LINES = 15;
9233
9293
  var OUTPUT_CHARS = 1500;
9234
9294
  var TEXT_CHARS = 1500;
@@ -9280,7 +9340,7 @@ function workdirFiles(dir) {
9280
9340
  for (const name of readdirSync4(d)) {
9281
9341
  if (SKIP_DIRS.has(name))
9282
9342
  continue;
9283
- const p = join7(d, name);
9343
+ const p = join8(d, name);
9284
9344
  if (statSync2(p).isDirectory())
9285
9345
  walk(p);
9286
9346
  else
@@ -9780,7 +9840,7 @@ async function uploadRun(t, r) {
9780
9840
 
9781
9841
  // src/which.ts
9782
9842
  import { accessSync, constants, statSync as statSync3 } from "node:fs";
9783
- import { delimiter, join as join8, resolve } from "node:path";
9843
+ import { delimiter, join as join9, resolve } from "node:path";
9784
9844
  function resolveBin(bin) {
9785
9845
  const executable = (p) => {
9786
9846
  try {
@@ -9793,8 +9853,8 @@ function resolveBin(bin) {
9793
9853
  if (bin.includes("/"))
9794
9854
  return executable(bin) ? resolve(bin) : undefined;
9795
9855
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
9796
- if (dir && executable(join8(dir, bin)))
9797
- return join8(dir, bin);
9856
+ if (dir && executable(join9(dir, bin)))
9857
+ return join9(dir, bin);
9798
9858
  }
9799
9859
  return;
9800
9860
  }
@@ -9881,7 +9941,7 @@ function newRunId() {
9881
9941
  return `${ts}-${randomBytes2(2).toString("hex")}`;
9882
9942
  }
9883
9943
  function writeRunJson(r) {
9884
- writeFileSync4(join9(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
9944
+ writeFileSync4(join10(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
9885
9945
  `);
9886
9946
  }
9887
9947
  async function uploadPreflight(opts, ui, project, runGroup, git) {
@@ -9922,14 +9982,14 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
9922
9982
  };
9923
9983
  }
9924
9984
  }
9925
- function runFill(runId, n, testName, out, dir) {
9985
+ function runFill(runId, n, testName, out, agentDir) {
9926
9986
  return {
9927
9987
  "run.id": runId,
9928
9988
  "run.n": String(n),
9929
9989
  "test.name": testName,
9930
9990
  FIREGOAT_OUT: out,
9931
- FIREGOAT_WORKDIR: join9(dir, "work"),
9932
- FIREGOAT_HOME: join9(dir, "home")
9991
+ FIREGOAT_WORKDIR: join10(agentDir, "work"),
9992
+ FIREGOAT_HOME: join10(agentDir, "home")
9933
9993
  };
9934
9994
  }
9935
9995
  function renderHarnessArgs(test, vars, fill) {
@@ -9962,10 +10022,11 @@ function showsHarness(harnesses) {
9962
10022
  }
9963
10023
  var DRY_RUN_ID = "<runId>";
9964
10024
  function dryRun(opts) {
9965
- const out = join9(opts.runsRoot, DRY_RUN_ID, "out");
10025
+ const agentDir = join10(opts.agentDirs, "<random>");
10026
+ const out = join10(agentDir, "out");
9966
10027
  const runs = opts.plan.map((p) => {
9967
10028
  const test = opts.tests.find((t) => t.name === p.test);
9968
- const fill = runFill(DRY_RUN_ID, p.n, test.name, out, join9(opts.runsRoot, DRY_RUN_ID, test.name, String(p.n)));
10029
+ const fill = runFill(DRY_RUN_ID, p.n, test.name, out, agentDir);
9969
10030
  renderEnv(test.env, opts.vars, fill);
9970
10031
  return {
9971
10032
  ...p,
@@ -9986,11 +10047,15 @@ function dryRun(opts) {
9986
10047
  async function runOne(opts) {
9987
10048
  const { test, n } = opts;
9988
10049
  const started = Date.now();
9989
- const dir = join9(opts.runDir, test.name, String(n));
9990
- const workdir = join9(dir, "work");
9991
- mkdirSync4(workdir, { recursive: true });
9992
- const { home, envFile } = makeRunHome(dir);
9993
- const fill = runFill(opts.runId, n, test.name, opts.out, dir);
10050
+ const dir = join10(opts.runDir, test.name, String(n));
10051
+ mkdirSync5(dir, { recursive: true });
10052
+ const agentDir = makeAgentDir(opts.agentDirs);
10053
+ const workdir = join10(agentDir, "work");
10054
+ mkdirSync5(workdir);
10055
+ const { home, envFile } = makeRunHome(agentDir);
10056
+ linkDir(workdir, join10(dir, "work"));
10057
+ linkDir(home, join10(dir, "home"));
10058
+ const fill = runFill(opts.runId, n, test.name, opts.out, agentDir);
9994
10059
  const r = {
9995
10060
  test: test.name,
9996
10061
  n,
@@ -10007,11 +10072,11 @@ async function runOne(opts) {
10007
10072
  workdir,
10008
10073
  home,
10009
10074
  session: null,
10010
- transcript: join9(dir, "transcript.txt"),
10011
- stderr: join9(dir, "stderr.txt"),
10012
- checksLog: join9(dir, "checks.txt"),
10075
+ transcript: join10(dir, "transcript.txt"),
10076
+ stderr: join10(dir, "stderr.txt"),
10077
+ checksLog: join10(dir, "checks.txt"),
10013
10078
  cleanup: [],
10014
- cleanupLog: join9(dir, "cleanup.txt"),
10079
+ cleanupLog: join10(dir, "cleanup.txt"),
10015
10080
  num_turns: null,
10016
10081
  cost: null,
10017
10082
  duration_ms: 0,
@@ -10024,7 +10089,9 @@ async function runOne(opts) {
10024
10089
  ...env,
10025
10090
  ...homeEnv(home),
10026
10091
  FIREGOAT_WORKDIR: workdir,
10027
- FIREGOAT_HOME: home
10092
+ FIREGOAT_HOME: home,
10093
+ FIREGOAT_RUN_DIR: dir,
10094
+ FIREGOAT_REPO: opts.repo
10028
10095
  };
10029
10096
  const steps = renderRunSteps(test, opts, opts.vars, fill);
10030
10097
  const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile);
@@ -10041,6 +10108,9 @@ async function runOne(opts) {
10041
10108
  writeRunJson(r);
10042
10109
  return r;
10043
10110
  }
10111
+ function withoutOldPwd(env) {
10112
+ return Object.fromEntries(Object.entries(env).filter(([k]) => k !== "OLDPWD"));
10113
+ }
10044
10114
  async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
10045
10115
  const { test, n } = opts;
10046
10116
  const { dir, workdir } = r;
@@ -10050,7 +10120,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
10050
10120
  commands: steps.install,
10051
10121
  cwd: workdir,
10052
10122
  env: stepEnv,
10053
- logPath: join9(dir, "install.txt")
10123
+ logPath: join10(dir, "install.txt")
10054
10124
  });
10055
10125
  if (error)
10056
10126
  return ["error", `not graded: ${error}`];
@@ -10062,12 +10132,13 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
10062
10132
  prompt: r.prompt,
10063
10133
  cwd: workdir,
10064
10134
  env: {
10065
- ...hostEnv(),
10135
+ ...withoutOldPwd(hostEnv()),
10066
10136
  ...env,
10067
10137
  ...harness.env({
10068
10138
  configDir: opts.configDir,
10069
10139
  run: { home: r.home, homeEnvFile }
10070
10140
  }),
10141
+ PWD: workdir,
10071
10142
  PATH: opts.stepEnv.PATH,
10072
10143
  FIREGOAT_OUT: opts.out,
10073
10144
  FIREGOAT_WORKDIR: workdir,
@@ -10144,7 +10215,7 @@ function uploadedSummary(origin, results, runDir) {
10144
10215
  return;
10145
10216
  const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
10146
10217
  if (urls.length > MAX_LISTED_URLS) {
10147
- console.log(`${head}: each run's upload.url in ${join9(runDir, "summary.json")}`);
10218
+ console.log(`${head}: each run's upload.url in ${join10(runDir, "summary.json")}`);
10148
10219
  return;
10149
10220
  }
10150
10221
  console.log(`${head}:`);
@@ -10245,7 +10316,7 @@ async function evalCommand(args, root) {
10245
10316
  plan: planRuns(tests, harnessNames, models, runs),
10246
10317
  showHarness,
10247
10318
  vars,
10248
- runsRoot: join9(repo?.mainCheckout ?? root, ".firegoat", "runs"),
10319
+ agentDirs: projectAgentDir(repo?.mainCheckout ?? root),
10249
10320
  json: opts.json,
10250
10321
  say
10251
10322
  });
@@ -10271,11 +10342,13 @@ async function evalCommand(args, root) {
10271
10342
  } catch (e) {
10272
10343
  return fatal(EXIT.usage, e.message, "config");
10273
10344
  }
10274
- const firegoatDir = ensureFiregoatDir(repo?.mainCheckout ?? root);
10345
+ const project = repo?.mainCheckout ?? root;
10346
+ const firegoatDir = ensureFiregoatDir(project);
10347
+ const agentDirs = ensureProjectAgentDir(project);
10275
10348
  const configDirs = new Map;
10276
10349
  for (const h of harnesses) {
10277
- const configDir = join9(firegoatDir, h.configDirName);
10278
- mkdirSync4(configDir, { recursive: true });
10350
+ const configDir = join10(agentDirs, h.configDirName);
10351
+ mkdirSync5(configDir, { recursive: true });
10279
10352
  configDirs.set(h, configDir);
10280
10353
  }
10281
10354
  ui.auth(harnesses.length === 1 ? harnesses[0].authLabel() : harnesses.map((h) => `${h.name}: ${h.authLabel()}`).join("; "));
@@ -10308,10 +10381,12 @@ async function evalCommand(args, root) {
10308
10381
  return fatal(EXIT.usage, e.message, "config");
10309
10382
  throw e;
10310
10383
  }
10311
- const runsRoot = join9(firegoatDir, "runs");
10312
- const runDir = join9(runsRoot, runId);
10313
- const out = join9(runDir, "out");
10314
- mkdirSync4(out, { recursive: true });
10384
+ const runsRoot = join10(firegoatDir, "runs");
10385
+ const runDir = join10(runsRoot, runId);
10386
+ mkdirSync5(runDir, { recursive: true });
10387
+ const out = join10(makeAgentDir(agentDirs), "out");
10388
+ mkdirSync5(out);
10389
+ linkDir(out, join10(runDir, "out"));
10315
10390
  const stepEnv = {
10316
10391
  ...hostEnv(),
10317
10392
  ...vars,
@@ -10336,7 +10411,7 @@ async function evalCommand(args, root) {
10336
10411
  ...stepEnv,
10337
10412
  ...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
10338
10413
  },
10339
- logPath: join9(runDir, "build.txt")
10414
+ logPath: join10(runDir, "build.txt")
10340
10415
  });
10341
10416
  const secs = Math.round((Date.now() - buildStarted) / 1000);
10342
10417
  ui.build(error ? "fail" : "ok", secs);
@@ -10363,6 +10438,8 @@ async function evalCommand(args, root) {
10363
10438
  shownHarness: row.harness,
10364
10439
  runId,
10365
10440
  runDir,
10441
+ agentDirs,
10442
+ repo: root,
10366
10443
  harness,
10367
10444
  bin: bins.get(harness),
10368
10445
  configDir: configDirs.get(harness),
@@ -10432,9 +10509,9 @@ async function evalCommand(args, root) {
10432
10509
  status,
10433
10510
  exitCode
10434
10511
  };
10435
- writeFileSync4(join9(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
10512
+ writeFileSync4(join10(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
10436
10513
  `);
10437
- appendFileSync(join9(runsRoot, "index.jsonl"), `${JSON.stringify({
10514
+ appendFileSync(join10(runsRoot, "index.jsonl"), `${JSON.stringify({
10438
10515
  runId,
10439
10516
  startedAt: new Date(evalStarted).toISOString(),
10440
10517
  dir: root,
@@ -10747,7 +10824,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
10747
10824
  // src/init.ts
10748
10825
  import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
10749
10826
  import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
10750
- import { basename as basename4, join as join10 } from "node:path";
10827
+ import { basename as basename4, join as join11 } from "node:path";
10751
10828
 
10752
10829
  // src/login.ts
10753
10830
  import { spawn as spawn2 } from "node:child_process";
@@ -11046,12 +11123,12 @@ function skillsDir(scope) {
11046
11123
  if (scope === "local")
11047
11124
  return ".claude/skills/";
11048
11125
  const config = process.env.CLAUDE_CONFIG_DIR?.trim();
11049
- return config ? `${join10(config, "skills")}/` : "~/.claude/skills/";
11126
+ return config ? `${join11(config, "skills")}/` : "~/.claude/skills/";
11050
11127
  }
11051
11128
  function template(product) {
11052
11129
  return `# firegoat.yaml: evals for how well coding agents use ${product}.
11053
11130
  # Each test runs your agent (claude by default) in an empty folder
11054
- # (.firegoat/runs/...) with \`prompt\` as the first message, then runs \`checks\`
11131
+ # (~/.firegoat/runs/...) with \`prompt\` as the first message, then runs \`checks\`
11055
11132
  # there. Reference: npx firegoat init --help
11056
11133
 
11057
11134
  # How many times to run each test.
@@ -11308,7 +11385,7 @@ ${INIT_HELP}`);
11308
11385
  console.log(rule);
11309
11386
  }
11310
11387
  ensureFiregoatDir(cwd);
11311
- const configPath = join10(cwd, CONFIG_FILE);
11388
+ const configPath = join11(cwd, CONFIG_FILE);
11312
11389
  const keep = existsSync6(configPath) && !force;
11313
11390
  if (!keep)
11314
11391
  writeFileSync5(configPath, template(basename4(cwd)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/firegoat-ai/firegoat-cli.git"
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "bun build src/cli.ts --target node --outfile dist/cli.js",
32
- "test": "bun test",
32
+ "test": "bun test --timeout 30000",
33
33
  "typecheck": "bun check",
34
34
  "lint": "biome check .",
35
35
  "format": "biome check --write .",