firegoat 0.28.0 → 0.30.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 +355 -251
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -22,6 +22,8 @@ In a terminal it asks where to install the firegoat-onboarding skill (global by
22
22
  npx firegoat@latest eval
23
23
  ```
24
24
 
25
+ Run by an agent, eval ends with a results block to paste as-is (also in `.firegoat/runs/<runId>/results.txt`): ask it to show you the results table.
26
+
25
27
  ## Example
26
28
 
27
29
  ```yaml
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.28.0",
6950
+ version: "0.30.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
@@ -7265,8 +7265,146 @@ import { homedir } from "node:os";
7265
7265
  import { join as join3 } from "node:path";
7266
7266
 
7267
7267
  // src/home.ts
7268
+ import { execFile } from "node:child_process";
7268
7269
  import { mkdirSync, writeFileSync } from "node:fs";
7269
7270
  import { join as join2 } from "node:path";
7271
+ import { promisify } from "node:util";
7272
+
7273
+ // src/process.ts
7274
+ import { execFileSync, spawn } from "node:child_process";
7275
+ var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
7276
+ 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)$/;
7277
+ var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
7278
+ function hostEnv() {
7279
+ return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
7280
+ }
7281
+ var GRACE_MS = 5000;
7282
+ var liveGroups = new Set;
7283
+ var signalsHooked = false;
7284
+ function hookSignals() {
7285
+ if (signalsHooked)
7286
+ return;
7287
+ signalsHooked = true;
7288
+ const onSignal = (signal) => {
7289
+ for (const g of liveGroups)
7290
+ try {
7291
+ process.kill(-g, "SIGKILL");
7292
+ } catch {}
7293
+ process.kill(process.pid, signal);
7294
+ };
7295
+ for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
7296
+ process.once(s, onSignal);
7297
+ }
7298
+ function forgetGroup(pgid) {
7299
+ liveGroups.delete(pgid);
7300
+ }
7301
+ function processTree(pid) {
7302
+ const children = new Map;
7303
+ const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
7304
+ encoding: "utf8"
7305
+ });
7306
+ for (const line of ps.split(`
7307
+ `)) {
7308
+ const [child, parent] = line.trim().split(/\s+/).map(Number);
7309
+ if (!child || parent === undefined)
7310
+ continue;
7311
+ children.set(parent, [...children.get(parent) ?? [], child]);
7312
+ }
7313
+ const tree = [pid];
7314
+ for (let i = 0;i < tree.length; i++)
7315
+ tree.push(...children.get(tree[i]) ?? []);
7316
+ return tree;
7317
+ }
7318
+ function killTree(pid, group) {
7319
+ if (group)
7320
+ try {
7321
+ process.kill(-pid, "SIGKILL");
7322
+ } catch {}
7323
+ let tree = [pid];
7324
+ let problem;
7325
+ try {
7326
+ tree = processTree(pid);
7327
+ } catch (e) {
7328
+ problem = e.message;
7329
+ }
7330
+ for (const p of tree)
7331
+ try {
7332
+ process.kill(p, "SIGKILL");
7333
+ } catch {}
7334
+ return problem;
7335
+ }
7336
+ function run(opts) {
7337
+ return new Promise((resolve) => {
7338
+ const detached = opts.detached === true;
7339
+ if (detached)
7340
+ hookSignals();
7341
+ const child = spawn(opts.cmd, opts.args, {
7342
+ cwd: opts.cwd,
7343
+ env: opts.env,
7344
+ stdio: ["ignore", "pipe", "pipe"],
7345
+ detached
7346
+ });
7347
+ if (child.pid !== undefined) {
7348
+ if (detached)
7349
+ liveGroups.add(child.pid);
7350
+ opts.onSpawn?.(child.pid);
7351
+ }
7352
+ let timedOut = false;
7353
+ let settled = false;
7354
+ const timer = setTimeout(() => {
7355
+ timedOut = true;
7356
+ if (child.pid === undefined)
7357
+ return;
7358
+ const problem = killTree(child.pid, detached);
7359
+ if (problem)
7360
+ opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
7361
+ `));
7362
+ }, opts.timeoutMs);
7363
+ const done = (r) => {
7364
+ if (settled)
7365
+ return;
7366
+ settled = true;
7367
+ clearTimeout(timer);
7368
+ resolve(r);
7369
+ };
7370
+ child.stdout.on("data", opts.onStdout);
7371
+ child.stderr.on("data", opts.onStderr);
7372
+ opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
7373
+ child.on("error", (error) => done({ code: null, timedOut, error }));
7374
+ child.on("close", (code) => done({ code, timedOut }));
7375
+ child.on("exit", (code) => setTimeout(() => {
7376
+ child.stdout.destroy();
7377
+ child.stderr.destroy();
7378
+ done({ code, timedOut });
7379
+ }, 2000).unref());
7380
+ });
7381
+ }
7382
+ async function runShell(opts) {
7383
+ opts.log.write(`$ ${opts.run}
7384
+ `);
7385
+ const r = await run({
7386
+ cmd: "/bin/sh",
7387
+ args: ["-c", opts.run],
7388
+ cwd: opts.cwd,
7389
+ env: opts.env,
7390
+ timeoutMs: opts.timeoutMs,
7391
+ onStdout: (c) => opts.log.write(c),
7392
+ onStderr: (c) => opts.log.write(c)
7393
+ });
7394
+ let error;
7395
+ if (r.error)
7396
+ error = `could not start /bin/sh: ${r.error.message}`;
7397
+ else if (r.timedOut)
7398
+ error = `timed out after ${opts.timeoutMs / 1000}s`;
7399
+ else if (r.code === null)
7400
+ error = "killed by a signal";
7401
+ opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
7402
+
7403
+ `);
7404
+ return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
7405
+ }
7406
+
7407
+ // src/home.ts
7270
7408
  function homeEnv(home) {
7271
7409
  return {
7272
7410
  HOME: home,
@@ -7287,6 +7425,23 @@ function makeRunHome(dir) {
7287
7425
  `).join(""));
7288
7426
  return { home, envFile };
7289
7427
  }
7428
+ async function makeRunKeychain(home) {
7429
+ const keychain = join2(home, "Library", "Keychains", "login.keychain-db");
7430
+ const env = { ...hostEnv(), HOME: home };
7431
+ mkdirSync(join2(home, "Library", "Keychains"), { recursive: true });
7432
+ for (const args of [
7433
+ ["create-keychain", "-p", "", keychain],
7434
+ ["set-keychain-settings", keychain]
7435
+ ]) {
7436
+ try {
7437
+ await promisify(execFile)("security", args, { env, timeout: 30000 });
7438
+ } catch (e) {
7439
+ const err = e;
7440
+ return `could not create the run's login keychain (security ${args[0]} ${keychain}): ${err.stderr?.trim() || err.message}`;
7441
+ }
7442
+ }
7443
+ return null;
7444
+ }
7290
7445
 
7291
7446
  // src/harnesses/codex.ts
7292
7447
  var FLAGS2 = [
@@ -7879,6 +8034,7 @@ function validHarnesses() {
7879
8034
  // src/config.ts
7880
8035
  var CONFIG_FILE = "firegoat.yaml";
7881
8036
  var DEFAULT_TIMEOUT = 600;
8037
+ var DEFAULT_PARALLEL = 4;
7882
8038
 
7883
8039
  class ConfigError extends Error {
7884
8040
  }
@@ -7916,6 +8072,7 @@ function parseConfig(text) {
7916
8072
  fail("(root)", "must be a mapping");
7917
8073
  onlyKeys(raw, "(root)", [
7918
8074
  "runs",
8075
+ "parallel",
7919
8076
  "harness",
7920
8077
  "model",
7921
8078
  "build",
@@ -7927,6 +8084,7 @@ function parseConfig(text) {
7927
8084
  "tests"
7928
8085
  ]);
7929
8086
  const runs = posInt(raw.runs, "runs", 1);
8087
+ const parallel = posInt(raw.parallel, "parallel", DEFAULT_PARALLEL);
7930
8088
  const harnesses = raw.harness === undefined ? [DEFAULT_HARNESS] : nameList(raw.harness, "harness", "a harness name");
7931
8089
  harnesses.forEach((h, i) => {
7932
8090
  if (!HARNESSES.has(h))
@@ -7990,6 +8148,7 @@ function parseConfig(text) {
7990
8148
  });
7991
8149
  return {
7992
8150
  runs,
8151
+ parallel,
7993
8152
  harnesses,
7994
8153
  models,
7995
8154
  build,
@@ -8173,6 +8332,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
8173
8332
  import {
8174
8333
  appendFileSync,
8175
8334
  createWriteStream as createWriteStream2,
8335
+ existsSync as existsSync6,
8176
8336
  mkdirSync as mkdirSync5,
8177
8337
  writeFileSync as writeFileSync5
8178
8338
  } from "node:fs";
@@ -8530,142 +8690,6 @@ function ensureFiregoatDir(root) {
8530
8690
  // src/git.ts
8531
8691
  import { spawnSync as spawnSync3 } from "node:child_process";
8532
8692
  import { basename, dirname } from "node:path";
8533
-
8534
- // src/process.ts
8535
- import { execFileSync, spawn } from "node:child_process";
8536
- var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
8537
- 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)$/;
8538
- var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
8539
- function hostEnv() {
8540
- return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
8541
- }
8542
- var GRACE_MS = 5000;
8543
- var liveGroups = new Set;
8544
- var signalsHooked = false;
8545
- function hookSignals() {
8546
- if (signalsHooked)
8547
- return;
8548
- signalsHooked = true;
8549
- const onSignal = (signal) => {
8550
- for (const g of liveGroups)
8551
- try {
8552
- process.kill(-g, "SIGKILL");
8553
- } catch {}
8554
- process.kill(process.pid, signal);
8555
- };
8556
- for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
8557
- process.once(s, onSignal);
8558
- }
8559
- function forgetGroup(pgid) {
8560
- liveGroups.delete(pgid);
8561
- }
8562
- function processTree(pid) {
8563
- const children = new Map;
8564
- const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
8565
- encoding: "utf8"
8566
- });
8567
- for (const line of ps.split(`
8568
- `)) {
8569
- const [child, parent] = line.trim().split(/\s+/).map(Number);
8570
- if (!child || parent === undefined)
8571
- continue;
8572
- children.set(parent, [...children.get(parent) ?? [], child]);
8573
- }
8574
- const tree = [pid];
8575
- for (let i = 0;i < tree.length; i++)
8576
- tree.push(...children.get(tree[i]) ?? []);
8577
- return tree;
8578
- }
8579
- function killTree(pid, group) {
8580
- if (group)
8581
- try {
8582
- process.kill(-pid, "SIGKILL");
8583
- } catch {}
8584
- let tree = [pid];
8585
- let problem;
8586
- try {
8587
- tree = processTree(pid);
8588
- } catch (e) {
8589
- problem = e.message;
8590
- }
8591
- for (const p of tree)
8592
- try {
8593
- process.kill(p, "SIGKILL");
8594
- } catch {}
8595
- return problem;
8596
- }
8597
- function run(opts) {
8598
- return new Promise((resolve) => {
8599
- const detached = opts.detached === true;
8600
- if (detached)
8601
- hookSignals();
8602
- const child = spawn(opts.cmd, opts.args, {
8603
- cwd: opts.cwd,
8604
- env: opts.env,
8605
- stdio: ["ignore", "pipe", "pipe"],
8606
- detached
8607
- });
8608
- if (child.pid !== undefined) {
8609
- if (detached)
8610
- liveGroups.add(child.pid);
8611
- opts.onSpawn?.(child.pid);
8612
- }
8613
- let timedOut = false;
8614
- let settled = false;
8615
- const timer = setTimeout(() => {
8616
- timedOut = true;
8617
- if (child.pid === undefined)
8618
- return;
8619
- const problem = killTree(child.pid, detached);
8620
- if (problem)
8621
- opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
8622
- `));
8623
- }, opts.timeoutMs);
8624
- const done = (r) => {
8625
- if (settled)
8626
- return;
8627
- settled = true;
8628
- clearTimeout(timer);
8629
- resolve(r);
8630
- };
8631
- child.stdout.on("data", opts.onStdout);
8632
- child.stderr.on("data", opts.onStderr);
8633
- opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
8634
- child.on("error", (error) => done({ code: null, timedOut, error }));
8635
- child.on("close", (code) => done({ code, timedOut }));
8636
- child.on("exit", (code) => setTimeout(() => {
8637
- child.stdout.destroy();
8638
- child.stderr.destroy();
8639
- done({ code, timedOut });
8640
- }, 2000).unref());
8641
- });
8642
- }
8643
- async function runShell(opts) {
8644
- opts.log.write(`$ ${opts.run}
8645
- `);
8646
- const r = await run({
8647
- cmd: "/bin/sh",
8648
- args: ["-c", opts.run],
8649
- cwd: opts.cwd,
8650
- env: opts.env,
8651
- timeoutMs: opts.timeoutMs,
8652
- onStdout: (c) => opts.log.write(c),
8653
- onStderr: (c) => opts.log.write(c)
8654
- });
8655
- let error;
8656
- if (r.error)
8657
- error = `could not start /bin/sh: ${r.error.message}`;
8658
- else if (r.timedOut)
8659
- error = `timed out after ${opts.timeoutMs / 1000}s`;
8660
- else if (r.code === null)
8661
- error = "killed by a signal";
8662
- opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
8663
-
8664
- `);
8665
- return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
8666
- }
8667
-
8668
- // src/git.ts
8669
8693
  function git(dir, args) {
8670
8694
  const r = spawnSync3("git", args, {
8671
8695
  cwd: dir,
@@ -8834,6 +8858,9 @@ firegoat.yaml, committed.
8834
8858
 
8835
8859
  firegoat.yaml reference:
8836
8860
  runs: 1 Runs per test
8861
+ parallel: 4 Runs at once (default 4; 1 runs them one after
8862
+ another, e.g. when tests share one cloud
8863
+ account); eval --parallel overrides
8837
8864
  harness: <name> | [<name>, ...]
8838
8865
  The agent CLI: claude-code (default), codex-cli
8839
8866
  or grok-cli.
@@ -8946,7 +8973,11 @@ firegoat.yaml reference:
8946
8973
  Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
8947
8974
  transcript, greppable line by line (format: eval --help, Transcript). So
8948
8975
  '^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
8949
- $FIREGOAT_SESSION (the harness's full session file, JSON lines).
8976
+ $FIREGOAT_SESSION (the harness's full session file, JSON lines). cleanup gets
8977
+ $FIREGOAT_TRANSCRIPT once the agent has run (also after a claude error or a
8978
+ timeout), and $FIREGOAT_SESSION when the harness saved a session file; neither
8979
+ is set when install failed before the agent started, so a cleanup that reads
8980
+ them should fail on an unset one: grep ... "\${FIREGOAT_TRANSCRIPT:?}".
8950
8981
 
8951
8982
  The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
8952
8983
  (npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
@@ -9102,10 +9133,13 @@ Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
9102
9133
  $FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
9103
9134
  collide. claude itself keeps your real HOME (its login is there) and sources
9104
9135
  home.env next to it (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
9105
- checks and cleanup get the run's HOME directly; build keeps yours. Limits: claude's
9106
- own Read/Write tools expand ~ to your real home; getpwuid lookups (os.userInfo,
9107
- python pwd) still see it; keychain logins (gh, docker, git credential helpers) look
9108
- logged out inside a run, so log tools in from install: or with env: tokens; the
9136
+ checks and cleanup get the run's HOME directly; build keeps yours. On macOS the run's
9137
+ home has its own empty, password-less login keychain (Library/Keychains), so a
9138
+ browser the agent starts doesn't ask you for one; your keychain is never touched.
9139
+ Limits: claude's own Read/Write tools expand ~ to your real home; getpwuid lookups
9140
+ (os.userInfo, python pwd) still see it; keychain logins (gh, docker, git credential
9141
+ helpers) look logged out inside a run, so log tools in from install: or with env:
9142
+ tokens; the
9109
9143
  fresh home has no .gitconfig, so set user.name/user.email in install: if the agent
9110
9144
  commits; claude's shell snapshot still sources your real ~/.zshrc.
9111
9145
 
@@ -9129,14 +9163,16 @@ firegoat.yaml, fixtures and build: stay in the current folder.
9129
9163
  --model NAME The harness's --model, replacing model: in firegoat.yaml;
9130
9164
  repeat it (--model a --model b) to run each test runs times
9131
9165
  per model
9132
- --parallel N Runs at once (default 4; 1 runs them one after another).
9166
+ --parallel N Runs at once (overrides parallel: in firegoat.yaml;
9167
+ default 4; 1 runs them one after another).
9133
9168
  Each run has its own HOME; state outside it (a fixed port,
9134
9169
  /tmp paths) can still collide: use 1 then
9135
9170
  --var KEY=value Fills {{vars.KEY}} (prompts, checks, build, install,
9136
9171
  cleanup); env for build, install, checks and cleanup
9137
9172
  --json The summary as JSON on stdout (or {error, exitCode} if eval
9138
9173
  stops early); progress goes to stderr. The verdict comes
9139
- last, after runs: totals {pass, fail, error}, status
9174
+ last, after runs: totals {pass, fail, error}, tier
9175
+ (S/A/B/C, as printed; null when no run was graded), status
9140
9176
  ("pass", "fail" or "error" = not graded) and exitCode
9141
9177
  --no-upload Keep runs local even when logged in to firegoat.ai
9142
9178
  --dry-run Check firegoat.yaml as eval would (test names, --var,
@@ -9167,6 +9203,14 @@ says "N uploads failed" on stderr. A file over 20 MiB fails that run's upload.
9167
9203
  Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
9168
9204
  (default https://firegoat.ai/evals). See firegoat login --help.
9169
9205
 
9206
+ Results block: without a terminal (an agent, a pipe, CI) and without --json, eval
9207
+ ends with the terminal's final timeline as one plain block (no color, at most 100
9208
+ columns): the logo header, a row per run (status, a ramp of one block per agent turn
9209
+ ending ▲ pass, ╳ fail or ╌ not graded, checks passed, cost, turns, time), its failed
9210
+ checks and trace URL, the totals and the tier ("GOATED · tier S" when all pass). It
9211
+ sits between two lines of ═, after a "Results (paste the block ...)" line; the last
9212
+ line says where it is saved, results.txt. Show it to your user as-is in a code block.
9213
+
9170
9214
  Output: .firegoat/runs/<runId>/
9171
9215
  out A symlink to $FIREGOAT_OUT,
9172
9216
  ~/.firegoat/runs/<hash>/<random>/out
@@ -9174,7 +9218,8 @@ Output: .firegoat/runs/<runId>/
9174
9218
  ran), git {branch, commit, dirty} (no git outside a
9175
9219
  repo), harnesses, upload {origin, email} (null when
9176
9220
  not uploading), runs (every run.json), then the
9177
- verdict: totals, status and exitCode
9221
+ verdict: totals, tier, status and exitCode
9222
+ results.txt The results block, also with --json or in a terminal
9178
9223
  ../index.jsonl One line per eval: runId, startedAt, dir, branch,
9179
9224
  commit, harnesses, passed, failed, notGraded, cost (null when
9180
9225
  any run's cost is unknown), uploaded, uploadFailed
@@ -9191,7 +9236,8 @@ Output: .firegoat/runs/<runId>/
9191
9236
  claudeSession (the session.jsonl uploaded: claude's
9192
9237
  own file, else <test>/<n>/session.jsonl made from the
9193
9238
  stream) and sessionId (its id, the trace's), stderr,
9194
- checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
9239
+ checksLog, cleanupLog; turns and cost (null when the
9240
+ harness reports none), durationMs, startedAt; upload ({status: "uploaded",
9195
9241
  url} | {status: "failed", error} | {status:
9196
9242
  "skipped", reason}, null when not uploading)
9197
9243
  <test>/<n>/transcript.txt What the agent did, below
@@ -10276,17 +10322,9 @@ function plainUI(write) {
10276
10322
  }
10277
10323
  };
10278
10324
  }
10279
- function ttyUI(out, opts) {
10280
- const p = painter(opts.color);
10281
- let rows = [];
10282
- let build = null;
10283
- let authText = "";
10284
- const notes = [];
10285
- let frame = 0;
10286
- let drawn = 0;
10287
- let finished = null;
10288
- const width = () => Math.min(out.columns || 80, 100);
10289
- const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
10325
+ var cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
10326
+ function timeline(t, p, w) {
10327
+ const { rows, build, finished, frame } = t;
10290
10328
  const statusWord = (r) => {
10291
10329
  switch (r.state) {
10292
10330
  case "pass":
@@ -10315,77 +10353,109 @@ function ttyUI(out, opts) {
10315
10353
  return p(C.gray, "○");
10316
10354
  }
10317
10355
  };
10318
- const lines = () => {
10319
- const w = width();
10320
- const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
10321
- const c = counts(rows);
10322
- const doneCount = c.pass + c.fail + c.error;
10323
- const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
10324
- const out = [
10325
- ...logoHeader(p, `firegoat ${opts.version} eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
10326
- rule
10327
- ];
10328
- if (authText)
10329
- out.push(p(C.gray, ` Auth ${authText}`));
10330
- for (const n of notes)
10331
- out.push(p(C.gray, ` ${cut(n, w - 2)}`));
10332
- if (build) {
10333
- const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
10334
- out.push(` ${build.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} Build${" ".repeat(Math.max(1, w - 20))}${mark}${build.seconds !== undefined ? ` ${build.seconds}s` : ""}`);
10335
- }
10336
- const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
10337
- const harnessCols = column((r) => r.harness?.length ?? 0);
10338
- const modelCols = column((r) => r.model?.length ?? 0);
10339
- rows.forEach((r, i) => {
10340
- out.push(p(C.gray, " │"));
10341
- const num = String(i + 1).padStart(2, "0");
10342
- const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
10343
- const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
10344
- const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
10345
- let detail = "";
10346
- if (r.state === "running")
10347
- detail = p(C.gray, `Turn ${r.turns}`);
10348
- else if (r.state === "pass" || r.state === "fail")
10349
- detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
10350
- else if (r.state === "error")
10351
- detail = p(C.gray, usage(r));
10352
- out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
10353
- if (r.state === "running" && r.command)
10354
- out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
10355
- if (r.state === "fail")
10356
- for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
10357
- out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
10358
- out.push(p(C.gray, ` │ ${ck.error ?? `exit ${ck.code}`}`));
10359
- }
10360
- if (r.state === "error")
10361
- out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
10362
- if (r.upload === "uploading")
10363
- out.push(p(C.gray, " │ ↑ Uploading…"));
10364
- else if (r.upload?.status === "uploaded")
10365
- out.push(` ${p(C.gray, "│")} ${p(C.cyan, `↑ ${r.upload.url}`)}`);
10366
- else if (r.upload)
10367
- out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
10368
- });
10369
- out.push(rule);
10370
- if (!finished) {
10371
- const track = Math.max(10, w - 34);
10372
- const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
10373
- const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
10374
- out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
10375
- return out;
10376
- }
10377
- const tr = tier(rows);
10378
- const goated = c.pass === rows.length && rows.length > 0;
10379
- const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
10380
- const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
10381
- const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
10382
- out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
10383
- const turns = totalTurns(rows);
10384
- out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
10385
- out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
10386
- out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
10356
+ const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
10357
+ const c = counts(rows);
10358
+ const doneCount = c.pass + c.fail + c.error;
10359
+ const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
10360
+ const out = [
10361
+ ...logoHeader(p, `firegoat ${t.version} eval · ${t.project}`, p(finished ? C.green : C.ember, badge), w),
10362
+ rule
10363
+ ];
10364
+ if (t.authText)
10365
+ out.push(p(C.gray, ` Auth ${t.authText}`));
10366
+ for (const n of t.notes)
10367
+ out.push(p(C.gray, ` ${cut(n, w - 2)}`));
10368
+ if (build) {
10369
+ const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
10370
+ out.push(` ${build.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} Build${" ".repeat(Math.max(1, w - 20))}${mark}${build.seconds !== undefined ? ` ${build.seconds}s` : ""}`);
10371
+ }
10372
+ const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
10373
+ const harnessCols = column((r) => r.harness?.length ?? 0);
10374
+ const modelCols = column((r) => r.model?.length ?? 0);
10375
+ const detailCols = w - 1 - (47 + (harnessCols > 0 ? harnessCols + 1 : 0) + (modelCols > 0 ? modelCols + 1 : 0));
10376
+ rows.forEach((r, i) => {
10377
+ out.push(p(C.gray, " │"));
10378
+ const num = String(i + 1).padStart(2, "0");
10379
+ const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
10380
+ const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
10381
+ const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
10382
+ let detail = "";
10383
+ if (r.state === "running")
10384
+ detail = p(C.gray, `Turn ${r.turns}`);
10385
+ else if (r.state === "pass" || r.state === "fail")
10386
+ detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, cut(usage(r), Math.max(1, detailCols - 12)))}`;
10387
+ else if (r.state === "error")
10388
+ detail = p(C.gray, cut(usage(r), Math.max(1, detailCols)));
10389
+ out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
10390
+ if (r.state === "running" && r.command)
10391
+ out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
10392
+ if (r.state === "fail")
10393
+ for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
10394
+ out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
10395
+ out.push(p(C.gray, ` │ ${cut(ck.error ?? `exit ${ck.code}`, w - 14)}`));
10396
+ }
10397
+ if (r.state === "error")
10398
+ out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
10399
+ if (r.upload === "uploading")
10400
+ out.push(p(C.gray, " │ ↑ Uploading…"));
10401
+ else if (r.upload?.status === "uploaded")
10402
+ out.push(` ${p(C.gray, "│")} ${p(C.cyan, `↑ ${r.upload.url}`)}`);
10403
+ else if (r.upload)
10404
+ out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
10405
+ });
10406
+ out.push(rule);
10407
+ if (!finished) {
10408
+ const track = Math.max(10, w - 34);
10409
+ const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
10410
+ const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
10411
+ out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
10387
10412
  return out;
10388
- };
10413
+ }
10414
+ const tr = tier(rows);
10415
+ const goated = c.pass === rows.length && rows.length > 0;
10416
+ const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
10417
+ const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
10418
+ const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
10419
+ out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
10420
+ const turns = totalTurns(rows);
10421
+ out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
10422
+ out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
10423
+ out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
10424
+ return out;
10425
+ }
10426
+ var RESULTS_WIDTH = 100;
10427
+ function resultsBlock(t) {
10428
+ return timeline({
10429
+ project: t.project,
10430
+ version: t.version,
10431
+ rows: t.rows,
10432
+ build: null,
10433
+ authText: "",
10434
+ notes: [],
10435
+ finished: { runId: t.runId, runsDir: t.runsDir, seconds: t.seconds },
10436
+ frame: 0
10437
+ }, painter(false), RESULTS_WIDTH);
10438
+ }
10439
+ function ttyUI(out, opts) {
10440
+ const p = painter(opts.color);
10441
+ let rows = [];
10442
+ let build = null;
10443
+ let authText = "";
10444
+ const notes = [];
10445
+ let frame = 0;
10446
+ let drawn = 0;
10447
+ let finished = null;
10448
+ const width = () => Math.min(out.columns || 80, 100);
10449
+ const lines = () => timeline({
10450
+ project: opts.project,
10451
+ version: opts.version,
10452
+ rows,
10453
+ build,
10454
+ authText,
10455
+ notes,
10456
+ finished,
10457
+ frame
10458
+ }, p, width());
10389
10459
  const rowsOf = (line) => {
10390
10460
  const visible = line.replace(ANSI, "");
10391
10461
  return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
@@ -10543,9 +10613,9 @@ async function uploadRun(t, r) {
10543
10613
  run: clip(c.run, MAX_TEXT, "…"),
10544
10614
  code: c.code
10545
10615
  })),
10546
- num_turns: r.num_turns,
10616
+ num_turns: r.turns,
10547
10617
  cost: r.cost,
10548
- duration_ms: r.duration_ms,
10618
+ duration_ms: r.durationMs,
10549
10619
  harness: r.harness,
10550
10620
  model,
10551
10621
  ...git && { git }
@@ -10624,7 +10694,6 @@ function parseArgs(args) {
10624
10694
  names: [],
10625
10695
  models: [],
10626
10696
  harnesses: [],
10627
- parallel: 4,
10628
10697
  json: false,
10629
10698
  noUpload: false,
10630
10699
  dryRun: false,
@@ -10835,9 +10904,9 @@ async function runOne(opts) {
10835
10904
  cleanup: [],
10836
10905
  cleanupLog: join10(dir, "cleanup.txt"),
10837
10906
  leftover: [],
10838
- num_turns: null,
10907
+ turns: null,
10839
10908
  cost: null,
10840
- duration_ms: 0,
10909
+ durationMs: 0,
10841
10910
  startedAt: new Date(started).toISOString(),
10842
10911
  upload: null
10843
10912
  };
@@ -10858,13 +10927,17 @@ async function runOne(opts) {
10858
10927
  r.cleanup = await runAllSteps({
10859
10928
  commands: steps.cleanup,
10860
10929
  cwd: workdir,
10861
- env: stepEnv,
10930
+ env: {
10931
+ ...stepEnv,
10932
+ ...existsSync6(r.transcript) && { FIREGOAT_TRANSCRIPT: r.transcript },
10933
+ ...r.session !== null && { FIREGOAT_SESSION: r.session }
10934
+ },
10862
10935
  logPath: r.cleanupLog
10863
10936
  });
10864
10937
  r.leftover = await stopLeftovers({ group: agent.group, workdir });
10865
10938
  r.status = status;
10866
10939
  r.reason = reason;
10867
- r.duration_ms = Date.now() - started;
10940
+ r.durationMs = Date.now() - started;
10868
10941
  writeRunJson(r);
10869
10942
  return r;
10870
10943
  }
@@ -10874,6 +10947,11 @@ function withoutOldPwd(env) {
10874
10947
  async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
10875
10948
  const { test, n } = opts;
10876
10949
  const { dir, workdir } = r;
10950
+ if (process.platform === "darwin") {
10951
+ const error = await makeRunKeychain(r.home);
10952
+ if (error)
10953
+ return ["error", `not graded: ${error}`];
10954
+ }
10877
10955
  if (steps.install.length > 0) {
10878
10956
  const error = await runSteps({
10879
10957
  what: "install",
@@ -10947,7 +11025,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
10947
11025
  final: outcome.final,
10948
11026
  workdir
10949
11027
  });
10950
- r.num_turns = outcome.turns;
11028
+ r.turns = outcome.turns;
10951
11029
  r.cost = outcome.cost;
10952
11030
  if (!outcome.ok)
10953
11031
  return ["error", `not graded: ${outcome.reason}`];
@@ -10998,6 +11076,16 @@ function uploadedSummary(origin, results, runDir) {
10998
11076
  for (const u of urls)
10999
11077
  console.log(` ${u}`);
11000
11078
  }
11079
+ var RESULTS_EDGE = "═".repeat(RESULTS_WIDTH);
11080
+ function printResults(block, file) {
11081
+ console.log("");
11082
+ console.log("Results (paste the block between the ═ lines as-is, in a code block):");
11083
+ console.log(RESULTS_EDGE);
11084
+ for (const line of block)
11085
+ console.log(line);
11086
+ console.log(RESULTS_EDGE);
11087
+ console.log(`Results block saved to ${file}`);
11088
+ }
11001
11089
  async function evalCommand(args, root) {
11002
11090
  if (args.includes("--help") || args.includes("-h")) {
11003
11091
  console.log(EVAL_HELP);
@@ -11045,6 +11133,7 @@ async function evalCommand(args, root) {
11045
11133
  tests = tests.filter((t) => opts.names.includes(t.name));
11046
11134
  }
11047
11135
  const runs = opts.runs ?? config.runs;
11136
+ const parallel = opts.parallel ?? config.parallel;
11048
11137
  const models = opts.models.length > 0 ? opts.models : config.models;
11049
11138
  const harnessNames = opts.harnesses.length > 0 ? opts.harnesses : config.harnesses;
11050
11139
  if (opts.harnessBin !== undefined && harnessNames.length !== 1)
@@ -11055,7 +11144,7 @@ async function evalCommand(args, root) {
11055
11144
  testCount: tests.length,
11056
11145
  runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
11057
11146
  modelCount: models.length,
11058
- parallel: opts.parallel
11147
+ parallel
11059
11148
  });
11060
11149
  const vars = { ...config.vars, ...opts.vars };
11061
11150
  const missing = [
@@ -11215,11 +11304,11 @@ async function evalCommand(args, root) {
11215
11304
  results[i] = r;
11216
11305
  Object.assign(row, {
11217
11306
  state: r.status,
11218
- reportedTurns: r.num_turns,
11307
+ reportedTurns: r.turns,
11219
11308
  reason: r.reason,
11220
11309
  checks: r.checks,
11221
11310
  cost: r.cost,
11222
- seconds: Math.round(r.duration_ms / 1000),
11311
+ seconds: Math.round(r.durationMs / 1000),
11223
11312
  command: undefined
11224
11313
  });
11225
11314
  ui.update();
@@ -11237,7 +11326,7 @@ async function evalCommand(args, root) {
11237
11326
  while (next < rows.length)
11238
11327
  await runAt(next++);
11239
11328
  };
11240
- await Promise.all(Array.from({ length: Math.min(opts.parallel, rows.length) }, worker));
11329
+ await Promise.all(Array.from({ length: Math.min(parallel, rows.length) }, worker));
11241
11330
  const totals = { pass: 0, fail: 0, error: 0 };
11242
11331
  for (const r of results)
11243
11332
  totals[r.status]++;
@@ -11266,6 +11355,7 @@ async function evalCommand(args, root) {
11266
11355
  upload: upload && { origin: upload.origin, email: upload.email },
11267
11356
  runs: results,
11268
11357
  totals,
11358
+ tier: tier(rows),
11269
11359
  status,
11270
11360
  exitCode
11271
11361
  };
@@ -11286,12 +11376,8 @@ async function evalCommand(args, root) {
11286
11376
  uploadFailed: uploadsFailed
11287
11377
  })}
11288
11378
  `);
11289
- ui.done({
11290
- runId,
11291
- runsDir: runDir,
11292
- rows,
11293
- seconds: Math.round((Date.now() - evalStarted) / 1000)
11294
- });
11379
+ const seconds = Math.round((Date.now() - evalStarted) / 1000);
11380
+ ui.done({ runId, runsDir: runDir, rows, seconds });
11295
11381
  if (upload && !opts.json)
11296
11382
  uploadedSummary(upload.origin, results, runDir);
11297
11383
  for (const r of results)
@@ -11303,6 +11389,20 @@ async function evalCommand(args, root) {
11303
11389
  console.error(`firegoat eval: Warning: ${r.test}#${r.n} left ${r.leftover.length} process${r.leftover.length === 1 ? "" : "es"} running, stopped after cleanup: ${r.leftover.map((l) => `${l.pid} ${l.command}`).join("; ")} (run.json leftover)`);
11304
11390
  if (uploadsFailed > 0)
11305
11391
  console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
11392
+ const block = resultsBlock({
11393
+ project: basename2(root),
11394
+ version: package_default.version,
11395
+ rows,
11396
+ runId,
11397
+ runsDir: runDir,
11398
+ seconds
11399
+ });
11400
+ const resultsFile = join10(runDir, "results.txt");
11401
+ writeFileSync5(resultsFile, `${block.join(`
11402
+ `)}
11403
+ `);
11404
+ if (!opts.json && !stream.isTTY)
11405
+ printResults(block, resultsFile);
11306
11406
  if (opts.json)
11307
11407
  console.log(JSON.stringify(doc, null, 2));
11308
11408
  if (status === "error")
@@ -11586,7 +11686,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
11586
11686
 
11587
11687
  // src/init.ts
11588
11688
  import { spawn as spawn4, spawnSync as spawnSync4 } from "node:child_process";
11589
- import { existsSync as existsSync6, writeFileSync as writeFileSync6 } from "node:fs";
11689
+ import { existsSync as existsSync7, writeFileSync as writeFileSync6 } from "node:fs";
11590
11690
  import { basename as basename3, join as join11 } from "node:path";
11591
11691
 
11592
11692
  // src/login.ts
@@ -11900,6 +12000,10 @@ function template(product) {
11900
12000
  # How many times to run each test.
11901
12001
  runs: 1
11902
12002
 
12003
+ # Optional: how many runs at once (default 4). 1 runs them one after another,
12004
+ # e.g. when tests share one cloud account. eval --parallel N overrides it.
12005
+ # parallel: 1
12006
+
11903
12007
  # Optional: the agent CLI each test runs on: claude-code (default), codex-cli or grok-cli.
11904
12008
  # A list runs every test \`runs\` times per harness. eval --harness NAME overrides it.
11905
12009
  # harness: [claude-code, codex-cli]
@@ -11998,7 +12102,7 @@ function copyToClipboard(text) {
11998
12102
  });
11999
12103
  }
12000
12104
  var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
12001
- function cut(s, n) {
12105
+ function cut2(s, n) {
12002
12106
  return s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}~`;
12003
12107
  }
12004
12108
 
@@ -12013,8 +12117,8 @@ class Timeline {
12013
12117
  line(mark, n, label, status = "") {
12014
12118
  const head = ` ${mark} ${this.paint("90", String(n).padStart(2, "0"))} `;
12015
12119
  if (!status)
12016
- return head + cut(label, this.width - 9);
12017
- return `${head}${label.padEnd(28)}${this.paint("90", cut(status, this.width - 37))}`;
12120
+ return head + cut2(label, this.width - 9);
12121
+ return `${head}${label.padEnd(28)}${this.paint("90", cut2(status, this.width - 37))}`;
12018
12122
  }
12019
12123
  done(n, label, status) {
12020
12124
  return this.line(this.paint("32", "●"), n, label, status);
@@ -12037,7 +12141,7 @@ class Timeline {
12037
12141
  return select(block, options.length, (current) => [
12038
12142
  this.line(this.paint("38;5;208", "◉"), n, question),
12039
12143
  ...options.map(([label, note], i) => {
12040
- const text = cut(`${label.padEnd(cols)}${note}`, room);
12144
+ const text = cut2(`${label.padEnd(cols)}${note}`, room);
12041
12145
  return i === current ? `${Timeline.INDENT}${this.paint("38;5;208", `● ${text}`)}` : this.paint("90", `${Timeline.INDENT}○ ${text}`);
12042
12146
  }),
12043
12147
  this.paint("90", `${Timeline.INDENT}Up/down to move, enter to pick`)
@@ -12162,7 +12266,7 @@ ${INIT_HELP}`);
12162
12266
  }
12163
12267
  ensureFiregoatDir(cwd);
12164
12268
  const configPath = join11(cwd, CONFIG_FILE);
12165
- const keep = existsSync6(configPath) && !force;
12269
+ const keep = existsSync7(configPath) && !force;
12166
12270
  if (!keep)
12167
12271
  writeFileSync6(configPath, template(basename3(cwd)));
12168
12272
  if (ui) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/firegoat-ai/firegoat-cli.git"