firegoat 0.8.0 → 0.9.1

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 +72 -50
  2. package/dist/cli.js +474 -1344
  3. package/package.json +4 -6
package/dist/cli.js CHANGED
@@ -6947,19 +6947,17 @@ var require_public_api = __commonJS(function(exports) {
6947
6947
  // package.json
6948
6948
  var package_default = {
6949
6949
  name: "firegoat",
6950
- version: "0.8.0",
6950
+ version: "0.9.1",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
- url: "git+https://github.com/team2027/firegoat-cli.git"
6953
+ url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
6954
6954
  },
6955
6955
  devDependencies: {
6956
6956
  "@biomejs/biome": "^2.5.15",
6957
6957
  "@types/bun": "latest",
6958
+ typescript: "^7",
6958
6959
  yaml: "^2.9.1"
6959
6960
  },
6960
- peerDependencies: {
6961
- typescript: "^7"
6962
- },
6963
6961
  bin: {
6964
6962
  firegoat: "dist/cli.js"
6965
6963
  },
@@ -6970,7 +6968,7 @@ var package_default = {
6970
6968
  files: [
6971
6969
  "dist"
6972
6970
  ],
6973
- homepage: "https://github.com/team2027/firegoat-cli#readme",
6971
+ homepage: "https://github.com/firegoat-ai/firegoat-cli#readme",
6974
6972
  license: "MIT",
6975
6973
  publishConfig: {
6976
6974
  access: "public"
@@ -6987,412 +6985,8 @@ var package_default = {
6987
6985
  packageManager: "bun@1.4.3"
6988
6986
  };
6989
6987
 
6990
- // src/claude-login.ts
6991
- import { spawnSync as spawnSync2 } from "node:child_process";
6992
- import { mkdirSync as mkdirSync3 } from "node:fs";
6993
-
6994
- // src/auth.ts
6995
- import {
6996
- chmodSync,
6997
- existsSync as existsSync2,
6998
- mkdirSync as mkdirSync2,
6999
- readFileSync,
7000
- writeFileSync
7001
- } from "node:fs";
7002
- import { join as join2 } from "node:path";
7003
-
7004
- // src/sandbox.ts
7005
- import { spawnSync } from "node:child_process";
7006
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
7007
- import { homedir } from "node:os";
7008
- import { basename, dirname, join } from "node:path";
7009
- var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
7010
- function firegoatHome() {
7011
- return process.env.FIREGOAT_HOME || join(homedir(), ".firegoat");
7012
- }
7013
- function claudeConfigDir() {
7014
- return join(firegoatHome(), "claude");
7015
- }
7016
- var TOOLCHAINS = [
7017
- ".nvm",
7018
- ".fnm",
7019
- ".volta",
7020
- ".bun",
7021
- ".deno",
7022
- ".cargo",
7023
- ".rustup",
7024
- ".pyenv",
7025
- ".rbenv",
7026
- ".asdf",
7027
- ".sdkman",
7028
- ".local/share/mise",
7029
- ".local/share/fnm",
7030
- ".local/share/claude",
7031
- ".local/share/uv",
7032
- "go",
7033
- "Library/pnpm"
7034
- ];
7035
- var TOOLCHAIN_SECRETS = [".cargo/credentials", ".cargo/credentials.toml"];
7036
- function real(p) {
7037
- return realpathSync(p);
7038
- }
7039
- function under(p, dir) {
7040
- return p === dir || p.startsWith(`${dir}/`);
7041
- }
7042
- function readableToolDirs(home, claudeBin) {
7043
- const dirs = new Set;
7044
- const add = (p) => {
7045
- if (existsSync(p))
7046
- dirs.add(real(p));
7047
- };
7048
- for (const t of TOOLCHAINS)
7049
- add(join(home, t));
7050
- for (const p of (process.env.PATH ?? "").split(":")) {
7051
- if (!p.startsWith("/") || !existsSync(p))
7052
- continue;
7053
- const r = real(p);
7054
- if (!under(r, home) || r === home)
7055
- continue;
7056
- const prefix = dirname(r);
7057
- dirs.add(basename(r) === "bin" && prefix !== home && prefix !== join(home, ".local") ? prefix : r);
7058
- }
7059
- add(dirname(dirname(process.execPath)));
7060
- add(dirname(claudeBin));
7061
- add(dirname(real(claudeBin)));
7062
- return [...dirs];
7063
- }
7064
- var q = (s) => JSON.stringify(s);
7065
- function sandboxProfile(opts) {
7066
- const home = real(homedir());
7067
- const reads = readableToolDirs(home, opts.claudeBin).map((d) => `(subpath ${q(d)})`);
7068
- return `(version 1)
7069
- (allow default)
7070
-
7071
- (deny file-read* (subpath ${q(home)}))
7072
- (allow file-read-metadata)
7073
- (allow file-read*
7074
- ${reads.join(`
7075
- `)}
7076
- (subpath ${q(join(home, "Library/Keychains"))})
7077
- (regex ${q(`^${home}/Library/Preferences/com\\.apple\\.security`)})
7078
- (regex ${q(`^${home}/Library/Preferences/\\.GlobalPreferences`)}))
7079
- (deny file-read* file-write*
7080
- ${TOOLCHAIN_SECRETS.map((s) => `(literal ${q(join(home, s))})`).join(`
7081
- `)})
7082
-
7083
- (deny file-write* (subpath "/"))
7084
- (allow file-write*
7085
- (subpath "/dev")
7086
- (subpath "/private/tmp")
7087
- (subpath "/private/var/folders"))
7088
- (allow file-read* file-write*
7089
- (subpath ${q(real(opts.configDir))})
7090
- (subpath ${q(real(opts.runRoot))}))
7091
-
7092
- (deny file-read* file-write* (subpath ${q(real(opts.repo))}))
7093
- `;
7094
- }
7095
- function agentEnv(runRoot, apiKey) {
7096
- const env = { ...process.env };
7097
- delete env.ANTHROPIC_API_KEY;
7098
- if (apiKey)
7099
- env.ANTHROPIC_API_KEY = apiKey;
7100
- const cache = join(runRoot, "cache");
7101
- const tmp = join(runRoot, "tmp");
7102
- mkdirSync(cache, { recursive: true });
7103
- mkdirSync(tmp, { recursive: true });
7104
- return {
7105
- ...env,
7106
- CLAUDE_CONFIG_DIR: claudeConfigDir(),
7107
- TMPDIR: tmp,
7108
- XDG_CACHE_HOME: cache,
7109
- npm_config_cache: join(cache, "npm"),
7110
- npm_config_store_dir: join(cache, "pnpm-store"),
7111
- YARN_CACHE_FOLDER: join(cache, "yarn"),
7112
- BUN_INSTALL_CACHE_DIR: join(cache, "bun"),
7113
- PIP_CACHE_DIR: join(cache, "pip"),
7114
- UV_CACHE_DIR: join(cache, "uv"),
7115
- GOMODCACHE: join(cache, "gomod"),
7116
- GOCACHE: join(cache, "gobuild"),
7117
- CARGO_HOME: join(cache, "cargo")
7118
- };
7119
- }
7120
- function wrap(profile, cmd, args) {
7121
- if (profile === null)
7122
- return { cmd, args };
7123
- return { cmd: SANDBOX_EXEC, args: ["-p", profile, cmd, ...args] };
7124
- }
7125
- function probeSandbox(opts) {
7126
- const outside = join(homedir(), `.firegoat-escape-probe-${process.pid}-${Date.now()}`);
7127
- const script = 'touch "$1" 2>/dev/null && echo home-write; cat "$2" >/dev/null 2>&1 && echo repo-read; touch "$3" 2>/dev/null && echo work-write; rm -f "$1" "$3"';
7128
- const { cmd, args } = wrap(opts.profile, "/bin/sh", [
7129
- "-c",
7130
- script,
7131
- "probe",
7132
- outside,
7133
- opts.repoFile,
7134
- join(opts.workdir, ".firegoat-probe")
7135
- ]);
7136
- const r = spawnSync(cmd, args, { encoding: "utf8", cwd: opts.workdir });
7137
- if (r.error)
7138
- return `sandbox probe could not start: ${r.error.message}`;
7139
- const got = r.stdout.trim().split(`
7140
- `).filter(Boolean);
7141
- if (got.includes("home-write") || got.includes("repo-read"))
7142
- return `sandbox does not hold (escaped: ${got.filter((g) => g !== "work-write").join(", ")}); refusing to run the agent`;
7143
- if (!got.includes("work-write"))
7144
- return `sandbox blocks writes in the workdir (stderr: ${r.stderr.trim() || "none"})`;
7145
- return;
7146
- }
7147
-
7148
- // src/auth.ts
7149
- var authFile = () => join2(firegoatHome(), "auth.json");
7150
- var keyFile = () => join2(firegoatHome(), "anthropic-api-key");
7151
-
7152
- class AuthError extends Error {
7153
- }
7154
- function readMethod() {
7155
- const file = authFile();
7156
- if (!existsSync2(file))
7157
- return;
7158
- let doc;
7159
- try {
7160
- doc = JSON.parse(readFileSync(file, "utf8"));
7161
- } catch (e) {
7162
- throw new AuthError(`${file} is not valid JSON: ${e.message}`);
7163
- }
7164
- const method = doc?.method;
7165
- if (method !== "claude" && method !== "api-key")
7166
- throw new AuthError(`${file}: method must be "claude" or "api-key", got ${JSON.stringify(method)}. Run: npx firegoat init`);
7167
- return method;
7168
- }
7169
- function saveMethod(method) {
7170
- mkdirSync2(firegoatHome(), { recursive: true });
7171
- writeFileSync(authFile(), `${JSON.stringify({ method }, null, 2)}
7172
- `);
7173
- }
7174
- function saveApiKey(apiKey) {
7175
- mkdirSync2(firegoatHome(), { recursive: true });
7176
- const file = keyFile();
7177
- writeFileSync(file, `${apiKey}
7178
- `, { mode: 384 });
7179
- chmodSync(file, 384);
7180
- saveMethod("api-key");
7181
- return file;
7182
- }
7183
- function looksLikeAnthropicKey(s) {
7184
- return /^sk-ant-[A-Za-z0-9_-]{20,}$/.test(s);
7185
- }
7186
- function resolveAuth(env) {
7187
- if (env.CI && env.ANTHROPIC_API_KEY)
7188
- return {
7189
- method: "api-key",
7190
- label: "Anthropic API key from ANTHROPIC_API_KEY (CI)",
7191
- apiKey: env.ANTHROPIC_API_KEY
7192
- };
7193
- const method = readMethod() ?? "claude";
7194
- if (method === "claude")
7195
- return {
7196
- method,
7197
- label: `Claude login in ${claudeConfigDir()}`
7198
- };
7199
- const file = keyFile();
7200
- if (!existsSync2(file))
7201
- throw new AuthError(`auth is set to api-key but ${file} is missing. Run: npx firegoat init`);
7202
- const apiKey = readFileSync(file, "utf8").trim();
7203
- if (!apiKey)
7204
- throw new AuthError(`${file} is empty. Run: npx firegoat init`);
7205
- return { method, label: `Anthropic API key in ${file}`, apiKey };
7206
- }
7207
-
7208
- // src/exit.ts
7209
- var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
7210
-
7211
- // src/help.ts
7212
- var EXIT_CODES = `Exit codes:
7213
- 0 every run's script checks passed
7214
- 1 a script check failed
7215
- 2 at least one run was not graded (claude missing or logged out, crash, timeout,
7216
- setup failed); wins over 1
7217
- 3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)`;
7218
- function topHelp(version) {
7219
- return `firegoat ${version}: local AX evals. A coding agent gets one prompt in a fresh,
7220
- sandboxed folder; firegoat records the transcript and runs your script checks.
7221
-
7222
- Typical flow (for an agent driving firegoat on behalf of a human):
7223
- 1. npx firegoat init --yes write firegoat.yaml (read: npx firegoat init --help)
7224
- 2. edit firegoat.yaml real user problems as prompts, an end_goal, checks
7225
- 3. ask the human to run once: npx firegoat claude login (opens a browser)
7226
- 4. npx firegoat eval --json run the tests; JSON summary on stdout
7227
- 5. judge each run's end_goal yourself, from its run.json, transcript and
7228
- workdir: firegoat only runs the script checks
7229
-
7230
- Commands:
7231
- init write firegoat.yaml; choose how the agent signs in
7232
- claude login sign claude in to firegoat's own config dir (interactive, once)
7233
- eval run the tests
7234
- open open (or --print) a run's folder
7235
-
7236
- Run firegoat <command> --help for details and examples.
7237
-
7238
- ${EXIT_CODES}`;
7239
- }
7240
- var INIT_HELP = `Usage: firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
7241
-
7242
- Writes firegoat.yaml in the current folder with starter tests from what the repo
7243
- ships (package.json, pyproject.toml, go.mod, Cargo.toml, README), and adds
7244
- .firegoat/ to .gitignore. Edit the tests afterwards; they are templates.
7245
-
7246
- --yes, -y don't ask questions (default when stdin is not a terminal)
7247
- --force, -f overwrite an existing firegoat.yaml
7248
- --skills also run: npx skills add team2027/skills --skill firegoat-onboarding
7249
- --skill suggesting-eval-prompts (into .claude/skills/)
7250
- --auth claude the agent signs in with a Claude login: runs \`firegoat claude login\`
7251
- (interactive: a human must complete it in the browser)
7252
- --auth api-key the agent signs in with an Anthropic API key, read from stdin when
7253
- piped (else asked for without echo) and stored in
7254
- ~/.firegoat/anthropic-api-key (mode 0600), never in the repo
7255
- Without --auth, init asks (unless --yes). In CI, ANTHROPIC_API_KEY in
7256
- the env wins.
7257
-
7258
- firegoat.yaml reference:
7259
- agents: [claude-code] only claude-code for now
7260
- runs: 1 runs per test
7261
- setup: <sh> | [<sh>, ...] once per eval, in the repo, on the host; whatever it
7262
- writes to $FIREGOAT_OUT is copied into each run and put
7263
- first on the agent's PATH
7264
- workdir_setup: <sh> | [...] in each fresh workdir before the agent starts
7265
- (env: $FIREGOAT_OUT, $FIREGOAT_WORKDIR, vars)
7266
- vars: {KEY: value} defaults for {{vars.KEY}}; --var KEY=value overrides
7267
- tests:
7268
- - name: quickstart letters, digits, '.', '_', '-'; unique
7269
- prompt: ... what a user would ask the agent; one concrete outcome
7270
- end_goal: ... what is true when the problem is solved; not graded by
7271
- firegoat, handed to you (goal: is a deprecated alias)
7272
- checks: optional shell commands, run in the workdir after the
7273
- - run: ... agent; exit 0 = pass; they set eval's exit code
7274
- timeout: 600 optional, seconds
7275
-
7276
- The agent's folder has no repo files. Checks get $FIREGOAT_WORKDIR,
7277
- $FIREGOAT_SESSION (stream-json transcript), $FIREGOAT_OUT and the vars.
7278
- end_goal is not graded by firegoat: it is handed to the caller to judge.
7279
-
7280
- Example:
7281
- setup: npm pack --pack-destination "$FIREGOAT_OUT"
7282
- workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
7283
- tests:
7284
- - name: first-call
7285
- prompt: >-
7286
- Using the acme SDK (already installed from the build under test), send a test
7287
- event and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
7288
- end_goal: >-
7289
- A script sends one event with the SDK's client (not raw HTTP) and prints its
7290
- id, without installing acme from the registry.
7291
- checks:
7292
- - run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_SESSION"
7293
- - run: |
7294
- ! grep -Eq 'npm (install|i) [^"]*acme' "$FIREGOAT_SESSION"
7295
-
7296
- Next: npx firegoat claude login (a human, once), then npx firegoat eval.
7297
-
7298
- Exit codes: 0 done, 2 the skills install or claude login failed, 3 usage error.`;
7299
- var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host]
7300
- [--claude-bin PATH]
7301
-
7302
- Runs each test headless: claude gets the prompt in a fresh folder outside the repo,
7303
- sandboxed on macOS (it can't read your home dir or the repo), with setup:'s outputs
7304
- on PATH. Then the test's script checks run in that folder.
7305
-
7306
- test.. only these tests (by name)
7307
- --runs N runs per test (overrides runs: in firegoat.yaml)
7308
- --var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
7309
- --json the summary as JSON on stdout; progress goes to stderr
7310
- --host no sandbox (required off macOS; use a throwaway container)
7311
- --claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
7312
-
7313
- Sign-in: the one chosen in firegoat init (Claude login or stored API key); in CI,
7314
- ANTHROPIC_API_KEY from the env. eval prints which. Not logged in = exit 2 with the
7315
- command to run.
7316
-
7317
- Output: .firegoat/runs/<runId>/
7318
- summary.json every run, plus totals and a grading hint
7319
- <test>/<n>/run.json one run: test, n, status and reason (from the checks),
7320
- prompt, end_goal, checks [{run, code}], and paths:
7321
- dir, workdir, session, stderr, checksLog
7322
- <test>/<n>/session.jsonl the agent's transcript, stream-json (one event per line)
7323
- setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
7324
-
7325
- Grading: firegoat runs only the script checks; they set the exit code. end_goal is
7326
- yours to judge, with your own process. session.jsonl is stream-json: the
7327
- last line has type "result" with the agent's final message; tool calls are
7328
- "tool_use" items in "assistant" messages; tool output is in "user" messages.
7329
-
7330
- Examples:
7331
- npx firegoat eval --json all tests, JSON summary on stdout
7332
- npx firegoat eval quickstart --runs 3 one test, three runs
7333
- npx firegoat eval --var SHA=$(git rev-parse HEAD)
7334
-
7335
- firegoat.yaml is looked up in the current folder, then parents up to the git root.
7336
- A run can take minutes (default timeout 600s per test).
7337
-
7338
- ${EXIT_CODES}`;
7339
- var OPEN_HELP = `Usage: firegoat open [runId] [--print]
7340
-
7341
- Opens a run's folder (.firegoat/runs/<runId>/) with the system opener; the latest
7342
- run by default. --print only prints the path (use this from an agent).
7343
-
7344
- Exit codes: 0 done, 2 the opener failed, 3 no such run.`;
7345
- var CLAUDE_LOGIN_HELP = `Usage: firegoat claude login [--claude-bin PATH]
7346
-
7347
- Signs claude in to firegoat's own config dir (~/.firegoat/claude), once per machine.
7348
- Evals then run on that login, never your ~/.claude. Interactive: it opens a browser,
7349
- so an agent should ask the human to run it. \`firegoat login\` is a deprecated alias.
7350
-
7351
- Exit codes: 0 logged in, 2 claude missing or the login failed, 3 usage error.`;
7352
-
7353
- // src/claude-login.ts
7354
- function claudeLoginCommand(args) {
7355
- let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
7356
- for (let i = 0;i < args.length; i++) {
7357
- const a = args[i];
7358
- if (a === "--help" || a === "-h") {
7359
- console.log(CLAUDE_LOGIN_HELP);
7360
- return EXIT.pass;
7361
- }
7362
- if (a === "--claude-bin" && args[i + 1] !== undefined)
7363
- claudeBin = args[++i];
7364
- else if (a.startsWith("--claude-bin="))
7365
- claudeBin = a.slice(13);
7366
- else {
7367
- console.error(`firegoat claude login: unknown argument: ${a}`);
7368
- return EXIT.usage;
7369
- }
7370
- }
7371
- const configDir = claudeConfigDir();
7372
- mkdirSync3(configDir, { recursive: true });
7373
- const env = {
7374
- ...process.env,
7375
- CLAUDE_CONFIG_DIR: configDir
7376
- };
7377
- delete env.ANTHROPIC_API_KEY;
7378
- console.error(`Signing claude in to ${configDir}. Evals run with this login, never your own ~/.claude.`);
7379
- const r = spawnSync2(claudeBin, ["auth", "login"], { env, stdio: "inherit" });
7380
- if (r.error) {
7381
- console.error(`firegoat claude login: could not run ${claudeBin}: ${r.error.message}`);
7382
- return EXIT.notGraded;
7383
- }
7384
- if (r.status !== 0) {
7385
- console.error(`firegoat claude login: claude auth login exited ${r.status}`);
7386
- return EXIT.notGraded;
7387
- }
7388
- saveMethod("claude");
7389
- console.log("logged in; evals now sign in with this Claude login. next: npx firegoat eval");
7390
- return EXIT.pass;
7391
- }
7392
-
7393
6988
  // src/config.ts
7394
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
7395
- import { dirname as dirname2, join as join3 } from "node:path";
6989
+ import { existsSync, readFileSync } from "node:fs";
7396
6990
 
7397
6991
  // node_modules/yaml/dist/index.js
7398
6992
  var composer = require_composer();
@@ -7443,7 +7037,6 @@ var $visitAsync = visit.visitAsync;
7443
7037
  // src/config.ts
7444
7038
  var CONFIG_FILE = "firegoat.yaml";
7445
7039
  var DEFAULT_TIMEOUT = 600;
7446
- var SUPPORTED_AGENTS = ["claude-code"];
7447
7040
 
7448
7041
  class ConfigError extends Error {
7449
7042
  }
@@ -7465,7 +7058,12 @@ function posInt(v, path, fallback) {
7465
7058
  fail(path, "must be a positive integer");
7466
7059
  return v;
7467
7060
  }
7468
- function parseConfig(text, warn = console.error) {
7061
+ function onlyKeys(v, path, allowed) {
7062
+ for (const k of Object.keys(v))
7063
+ if (!allowed.includes(k))
7064
+ fail(path === "(root)" ? k : `${path}.${k}`, `unknown key (allowed: ${allowed.join(", ")})`);
7065
+ }
7066
+ function parseConfig(text) {
7469
7067
  let raw;
7470
7068
  try {
7471
7069
  raw = $parse(text);
@@ -7474,17 +7072,8 @@ function parseConfig(text, warn = console.error) {
7474
7072
  }
7475
7073
  if (!isRecord(raw))
7476
7074
  fail("(root)", "must be a mapping");
7477
- const agents = raw.agents === undefined ? ["claude-code"] : raw.agents;
7478
- if (!Array.isArray(agents) || agents.length === 0)
7479
- fail("agents", "must be a non-empty list");
7480
- agents.forEach((a, i) => {
7481
- if (!SUPPORTED_AGENTS.includes(a)) {
7482
- fail(`agents[${i}]`, `unsupported agent ${JSON.stringify(a)} (v1 supports: ${SUPPORTED_AGENTS.join(", ")})`);
7483
- }
7484
- });
7075
+ onlyKeys(raw, "(root)", ["runs", "setup", "workdir_setup", "vars", "tests"]);
7485
7076
  const runs = posInt(raw.runs, "runs", 1);
7486
- if (raw.build !== undefined)
7487
- fail("build", "was replaced by setup: in firegoat 0.4.0: a shell command (or list) that writes what the agent gets to $FIREGOAT_OUT");
7488
7077
  const setup = commands(raw.setup, "setup");
7489
7078
  const workdirSetup = commands(raw.workdir_setup, "workdir_setup");
7490
7079
  const vars = parseVars(raw.vars);
@@ -7495,6 +7084,7 @@ function parseConfig(text, warn = console.error) {
7495
7084
  const p = `tests[${i}]`;
7496
7085
  if (!isRecord(t))
7497
7086
  fail(p, "must be a mapping");
7087
+ onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout"]);
7498
7088
  const name = str(t.name, `${p}.name`);
7499
7089
  if (!NAME.test(name))
7500
7090
  fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
@@ -7509,27 +7099,18 @@ function parseConfig(text, warn = console.error) {
7509
7099
  const cp = `${p}.checks[${j}]`;
7510
7100
  if (!isRecord(c))
7511
7101
  fail(cp, "must be a mapping");
7512
- if ("judge" in c)
7513
- fail(`${cp}.judge`, "plain-language checks were removed; put the criteria in end_goal (your agent judges it) or write a run: check");
7102
+ onlyKeys(c, cp, ["run"]);
7514
7103
  checks.push({ run: str(c.run, `${cp}.run`) });
7515
7104
  });
7516
- if (t.rubric !== undefined)
7517
- fail(`${p}.rubric`, "was removed in firegoat 0.8.0; put the criteria in end_goal");
7518
- if (t.goal !== undefined && t.end_goal !== undefined)
7519
- fail(`${p}.goal`, "use end_goal only (goal is its deprecated name)");
7520
- if (t.goal !== undefined)
7521
- warn(`${CONFIG_FILE}: ${p}.goal is deprecated; rename it to end_goal`);
7522
- const endGoalKey = t.goal !== undefined ? "goal" : "end_goal";
7523
7105
  return {
7524
7106
  name,
7525
7107
  prompt: str(t.prompt, `${p}.prompt`),
7526
- end_goal: str(t[endGoalKey], `${p}.${endGoalKey}`),
7108
+ end_goal: str(t.end_goal, `${p}.end_goal`),
7527
7109
  checks,
7528
7110
  timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
7529
7111
  };
7530
7112
  });
7531
7113
  return {
7532
- agents,
7533
7114
  runs,
7534
7115
  setup,
7535
7116
  workdirSetup,
@@ -7575,260 +7156,134 @@ function render(text, vars) {
7575
7156
  return v;
7576
7157
  });
7577
7158
  }
7578
- function loadConfig(dir, warn) {
7159
+ function loadConfig(dir) {
7579
7160
  const file = `${dir}/${CONFIG_FILE}`;
7580
- if (!existsSync3(file))
7161
+ if (!existsSync(file))
7581
7162
  throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
7582
- return parseConfig(readFileSync2(file, "utf8"), warn);
7583
- }
7584
- function findConfigDir(cwd) {
7585
- let dir = cwd;
7586
- for (;; ) {
7587
- if (existsSync3(join3(dir, CONFIG_FILE)))
7588
- return dir;
7589
- const parent = dirname2(dir);
7590
- if (existsSync3(join3(dir, ".git")) || parent === dir)
7591
- return cwd;
7592
- dir = parent;
7593
- }
7163
+ return parseConfig(readFileSync(file, "utf8"));
7594
7164
  }
7595
7165
 
7596
7166
  // src/eval.ts
7597
7167
  import { randomBytes } from "node:crypto";
7598
7168
  import {
7599
- createWriteStream as createWriteStream3,
7600
- existsSync as existsSync4,
7601
- mkdirSync as mkdirSync5,
7602
- mkdtempSync,
7603
- readFileSync as readFileSync3,
7604
- realpathSync as realpathSync2,
7169
+ createWriteStream as createWriteStream2,
7170
+ existsSync as existsSync3,
7171
+ mkdirSync as mkdirSync2,
7172
+ readdirSync,
7605
7173
  writeFileSync as writeFileSync2
7606
7174
  } from "node:fs";
7607
- import { tmpdir } from "node:os";
7608
- import { join as join6, resolve as resolve2 } from "node:path";
7175
+ import { delimiter as delimiter2, join as join3 } from "node:path";
7609
7176
 
7610
7177
  // src/claude.ts
7611
- import { spawn, spawnSync as spawnSync3 } from "node:child_process";
7612
- import { createWriteStream } from "node:fs";
7613
- import { createInterface } from "node:readline";
7178
+ import { spawn, spawnSync } from "node:child_process";
7614
7179
  var CLAUDE_FLAGS = [
7615
7180
  "--output-format",
7616
7181
  "stream-json",
7617
7182
  "--verbose",
7618
7183
  "--dangerously-skip-permissions",
7619
- "--no-session-persistence",
7620
7184
  "--strict-mcp-config",
7621
7185
  "--setting-sources",
7622
- ""
7186
+ "",
7187
+ "--disable-slash-commands"
7623
7188
  ];
7624
- function claudeArgv(prompt) {
7625
- return ["-p", prompt, ...CLAUDE_FLAGS];
7626
- }
7627
- var live = new Set;
7628
- function killGroup(child, signal) {
7629
- if (child.pid === undefined)
7630
- return;
7631
- try {
7632
- process.kill(-child.pid, signal);
7633
- } catch {
7634
- try {
7635
- child.kill(signal);
7636
- } catch {}
7637
- }
7638
- }
7639
- var signalsHooked = false;
7640
- function hookSignals() {
7641
- if (signalsHooked)
7642
- return;
7643
- signalsHooked = true;
7644
- for (const sig of ["SIGINT", "SIGTERM"]) {
7645
- process.on(sig, () => {
7646
- for (const c of live)
7647
- killGroup(c, "SIGKILL");
7648
- process.exit(sig === "SIGINT" ? 130 : 143);
7649
- });
7650
- }
7189
+ function claudeEnv(configDir) {
7190
+ return {
7191
+ ...process.env,
7192
+ CLAUDE_CONFIG_DIR: configDir,
7193
+ CLAUDE_SECURESTORAGE_CONFIG_DIR: ""
7194
+ };
7651
7195
  }
7652
- function spawnWatched(opts) {
7653
- hookSignals();
7196
+ function run(opts) {
7654
7197
  return new Promise((resolve) => {
7655
- let child;
7656
- try {
7657
- child = spawn(opts.cmd, opts.args, {
7658
- cwd: opts.cwd,
7659
- env: opts.env,
7660
- stdio: ["ignore", "pipe", "pipe"],
7661
- detached: true,
7662
- shell: false
7663
- });
7664
- } catch (e) {
7665
- resolve({
7666
- code: null,
7667
- timedOut: false,
7668
- spawnError: e
7669
- });
7670
- return;
7671
- }
7672
- live.add(child);
7198
+ const child = spawn(opts.cmd, opts.args, {
7199
+ cwd: opts.cwd,
7200
+ env: opts.env,
7201
+ stdio: ["ignore", "pipe", "pipe"]
7202
+ });
7673
7203
  let timedOut = false;
7674
- let done = false;
7675
- let exitCode = null;
7676
- let exited = false;
7677
- let linesDone = !opts.onLine;
7678
- let stderrDone = false;
7679
- let stdoutDone = !!opts.onLine;
7680
- let graceTimer;
7681
- const finish = (r) => {
7682
- if (done)
7204
+ let settled = false;
7205
+ const timer = setTimeout(() => {
7206
+ timedOut = true;
7207
+ child.kill("SIGKILL");
7208
+ }, opts.timeoutMs);
7209
+ const done = (r) => {
7210
+ if (settled)
7683
7211
  return;
7684
- done = true;
7212
+ settled = true;
7685
7213
  clearTimeout(timer);
7686
- clearTimeout(killTimer);
7687
- clearTimeout(graceTimer);
7688
- live.delete(child);
7689
7214
  resolve(r);
7690
7215
  };
7691
- const maybeFinish = () => {
7692
- if (exited && linesDone && stderrDone && stdoutDone)
7693
- finish({ code: exitCode, timedOut });
7694
- };
7695
- let killTimer;
7696
- const stop = () => {
7697
- killGroup(child, "SIGTERM");
7698
- killTimer = setTimeout(() => killGroup(child, "SIGKILL"), 3000);
7699
- };
7700
- const timer = setTimeout(() => {
7701
- timedOut = true;
7702
- stop();
7703
- }, opts.timeoutMs);
7704
- child.on("error", (err) => {
7705
- if (child.pid === undefined)
7706
- finish({ code: null, timedOut: false, spawnError: err });
7707
- });
7708
- child.on("exit", (code) => {
7709
- exited = true;
7710
- exitCode = code;
7711
- graceTimer = setTimeout(() => {
7712
- child.stdout?.destroy();
7713
- child.stderr?.destroy();
7714
- finish({ code: exitCode, timedOut });
7715
- }, 2000);
7716
- maybeFinish();
7717
- });
7718
- child.stderr?.on("data", opts.onStderr);
7719
- child.stderr?.on("close", () => {
7720
- stderrDone = true;
7721
- maybeFinish();
7722
- });
7723
- if (opts.onLine) {
7724
- const onLine = opts.onLine;
7725
- const rl = createInterface({
7726
- input: child.stdout
7727
- });
7728
- rl.on("line", (line) => {
7729
- if (onLine(line) === "kill")
7730
- stop();
7731
- });
7732
- rl.on("close", () => {
7733
- linesDone = true;
7734
- maybeFinish();
7735
- });
7736
- } else {
7737
- if (opts.onStdout)
7738
- child.stdout?.on("data", opts.onStdout);
7739
- child.stdout?.on("close", () => {
7740
- stdoutDone = true;
7741
- maybeFinish();
7742
- });
7743
- }
7216
+ child.stdout.on("data", opts.onStdout);
7217
+ child.stderr.on("data", opts.onStderr);
7218
+ child.on("error", (error) => done({ code: null, timedOut, error }));
7219
+ child.on("close", (code) => done({ code, timedOut }));
7220
+ child.on("exit", (code) => setTimeout(() => {
7221
+ child.stdout.destroy();
7222
+ child.stderr.destroy();
7223
+ done({ code, timedOut });
7224
+ }, 2000).unref());
7744
7225
  });
7745
7226
  }
7746
- function closeStream(s) {
7747
- return new Promise((r) => s.end(r));
7748
- }
7749
7227
  async function runClaude(opts) {
7750
- const session = createWriteStream(opts.sessionPath);
7751
- const stderr = createWriteStream(opts.stderrPath);
7752
- let first = true;
7753
- let badStart;
7754
- let result;
7755
- const spawned = await spawnWatched({
7756
- ...wrap(opts.profile, opts.bin, claudeArgv(opts.prompt)),
7228
+ let stdout = "";
7229
+ const exited = await run({
7230
+ cmd: opts.bin,
7231
+ args: ["-p", opts.prompt, ...CLAUDE_FLAGS],
7757
7232
  cwd: opts.cwd,
7758
7233
  env: opts.env,
7759
7234
  timeoutMs: opts.timeoutMs,
7760
- onStderr: (c) => stderr.write(c),
7761
- onLine: (line) => {
7762
- session.write(`${line}
7763
- `);
7764
- if (line.trim() === "")
7765
- return;
7766
- let msg;
7767
- try {
7768
- const parsed = JSON.parse(line);
7769
- if (typeof parsed === "object" && parsed !== null)
7770
- msg = parsed;
7771
- } catch {}
7772
- if (first) {
7773
- first = false;
7774
- if (msg?.type !== "system" || msg.subtype !== "init") {
7775
- badStart = `claude output did not start with a system/init stream-json line (got: ${line.slice(0, 120)})`;
7776
- return "kill";
7777
- }
7778
- if (msg.apiKeySource !== opts.keySource) {
7779
- badStart = opts.keySource === "none" ? `claude would use API key billing (apiKeySource=${JSON.stringify(msg.apiKeySource)}) instead of your Claude login. firegoat removes ANTHROPIC_API_KEY from the agent's env; check apiKeyHelper or other API key settings, then run \`npx firegoat claude login\`` : `claude did not use the configured API key (apiKeySource=${JSON.stringify(msg.apiKeySource)}, expected "ANTHROPIC_API_KEY")`;
7780
- return "kill";
7781
- }
7782
- return;
7783
- }
7784
- if (msg?.type === "result")
7785
- result = msg;
7786
- return;
7787
- }
7235
+ onStdout: (c) => {
7236
+ stdout += c.toString();
7237
+ },
7238
+ onStderr: (c) => opts.stderr.write(c)
7788
7239
  });
7789
- await Promise.all([closeStream(session), closeStream(stderr)]);
7790
- if (spawned.spawnError) {
7791
- const e = spawned.spawnError;
7792
- if (e.code === "ENOENT")
7793
- return {
7794
- ok: false,
7795
- reason: `claude binary not found: ${opts.bin} (install Claude Code, or pass --claude-bin / set FIREGOAT_CLAUDE_BIN)`
7796
- };
7240
+ if (exited.error)
7797
7241
  return {
7798
7242
  ok: false,
7799
- reason: `could not start ${opts.bin}: ${e.message}`
7243
+ reason: `could not start ${opts.bin}: ${exited.error.message}`
7800
7244
  };
7801
- }
7802
- if (badStart)
7803
- return { ok: false, reason: badStart };
7804
- if (spawned.timedOut)
7245
+ const events = stdout.split(`
7246
+ `).filter((l) => l.trim() !== "").map((l) => {
7247
+ try {
7248
+ return JSON.parse(l);
7249
+ } catch {
7250
+ return;
7251
+ }
7252
+ });
7253
+ const init = events[0];
7254
+ if (init?.type !== "system" || init.subtype !== "init")
7805
7255
  return {
7806
7256
  ok: false,
7807
- reason: `timed out after ${opts.timeoutMs / 1000}s`,
7808
- result
7257
+ reason: `claude did not start a session (exit ${exited.code ?? "by signal"}), see stderr.txt`
7809
7258
  };
7810
- if (first)
7259
+ const sessionId = String(init.session_id);
7260
+ const result = events.findLast((e) => e?.type === "result");
7261
+ if (exited.timedOut)
7811
7262
  return {
7812
7263
  ok: false,
7813
- reason: `claude produced no output (exit ${spawned.code ?? "signal"}), see stderr.txt`
7264
+ reason: `timed out after ${opts.timeoutMs / 1000}s`,
7265
+ result,
7266
+ sessionId
7814
7267
  };
7815
7268
  if (!result)
7816
7269
  return {
7817
7270
  ok: false,
7818
- reason: `claude exited ${spawned.code ?? "by signal"} without a result line, see stderr.txt`
7271
+ reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
7272
+ sessionId
7819
7273
  };
7820
7274
  if (result.is_error === true || result.subtype !== "success")
7821
7275
  return {
7822
7276
  ok: false,
7823
- reason: `claude result is an error (subtype=${result.subtype}, is_error=${result.is_error})`,
7824
- result
7277
+ reason: `claude result is an error (subtype=${result.subtype})`,
7278
+ result,
7279
+ sessionId
7825
7280
  };
7826
- return { ok: true, result };
7281
+ return { ok: true, result, sessionId };
7827
7282
  }
7828
7283
  async function runShell(opts) {
7829
7284
  opts.log.write(`$ ${opts.run}
7830
7285
  `);
7831
- const r = await spawnWatched({
7286
+ const r = await run({
7832
7287
  cmd: "/bin/sh",
7833
7288
  args: ["-c", opts.run],
7834
7289
  cwd: opts.cwd,
@@ -7838,8 +7293,8 @@ async function runShell(opts) {
7838
7293
  onStderr: (c) => opts.log.write(c)
7839
7294
  });
7840
7295
  let error;
7841
- if (r.spawnError)
7842
- error = `could not start /bin/sh: ${r.spawnError.message}`;
7296
+ if (r.error)
7297
+ error = `could not start /bin/sh: ${r.error.message}`;
7843
7298
  else if (r.timedOut)
7844
7299
  error = `timed out after ${opts.timeoutMs / 1000}s`;
7845
7300
  else if (r.code === null)
@@ -7847,23 +7302,17 @@ async function runShell(opts) {
7847
7302
  opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
7848
7303
 
7849
7304
  `);
7850
- const out = { run: opts.run, code: r.code };
7851
- if (error)
7852
- out.error = error;
7853
- if (r.spawnError)
7854
- out.notStarted = true;
7855
- return out;
7305
+ return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
7856
7306
  }
7857
7307
  function checkLogin(opts) {
7858
- const { cmd, args } = wrap(opts.profile, opts.bin, ["auth", "status"]);
7859
- const r = spawnSync3(cmd, args, {
7308
+ const r = spawnSync(opts.bin, ["auth", "status"], {
7860
7309
  cwd: opts.cwd,
7861
7310
  env: opts.env,
7862
7311
  encoding: "utf8",
7863
7312
  timeout: 30000
7864
7313
  });
7865
7314
  if (r.error)
7866
- return `could not run claude auth status: ${r.error.message}`;
7315
+ return `could not run ${opts.bin}: ${r.error.message}`;
7867
7316
  let status;
7868
7317
  try {
7869
7318
  status = JSON.parse(r.stdout);
@@ -7871,56 +7320,163 @@ function checkLogin(opts) {
7871
7320
  if (status?.loggedIn === true)
7872
7321
  return;
7873
7322
  if (status?.loggedIn === false)
7874
- return `claude is not logged in to firegoat's config dir (${opts.env.CLAUDE_CONFIG_DIR}). Run: npx firegoat claude login`;
7323
+ return "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again";
7875
7324
  return `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`;
7876
7325
  }
7877
7326
 
7327
+ // src/exit.ts
7328
+ var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
7329
+
7330
+ // src/firegoat-dir.ts
7331
+ import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
7332
+ import { join } from "node:path";
7333
+ function ensureFiregoatDir(root) {
7334
+ const dir = join(root, ".firegoat");
7335
+ mkdirSync(dir, { recursive: true });
7336
+ const ignore = join(dir, ".gitignore");
7337
+ if (!existsSync2(ignore))
7338
+ writeFileSync(ignore, `*
7339
+ `);
7340
+ return dir;
7341
+ }
7342
+
7343
+ // src/help.ts
7344
+ var EXIT_CODES = `Exit codes:
7345
+ 0 every run's script checks passed
7346
+ 1 a script check failed
7347
+ 2 at least one run was not graded (claude missing or logged out, crash, timeout,
7348
+ setup failed); wins over 1
7349
+ 3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)`;
7350
+ function topHelp(version) {
7351
+ return `firegoat ${version}: local AX evals. Your own claude gets one prompt in a fresh
7352
+ folder, with none of your claude config; firegoat saves the session and runs your
7353
+ script checks.
7354
+
7355
+ npx firegoat init create firegoat.yaml and .firegoat/, install the skills
7356
+ npx firegoat eval run the tests (claude must be logged in: run \`claude\` once)
7357
+
7358
+ Typical flow for a coding agent driving firegoat:
7359
+ 1. read npx firegoat init --help, then edit firegoat.yaml
7360
+ 2. npx firegoat eval --json
7361
+ 3. judge each run's end_goal from its run.json, session and workdir: firegoat only
7362
+ runs the script checks
7363
+
7364
+ Run firegoat <command> --help for details and examples.
7365
+
7366
+ ${EXIT_CODES}`;
7367
+ }
7368
+ var INIT_HELP = `Usage: firegoat init [--force] [--no-skills]
7369
+
7370
+ In the current folder: creates .firegoat/ (ignored by git through its own .gitignore)
7371
+ and a starter firegoat.yaml, installs the firegoat-onboarding and
7372
+ suggesting-eval-prompts skills into .claude/skills/ (npx skills add firegoat-ai/skills;
7373
+ asks first in a terminal), then prints a prompt to paste into your coding agent.
7374
+
7375
+ --force replace an existing firegoat.yaml
7376
+ --no-skills don't install the skills
7377
+
7378
+ firegoat.yaml reference:
7379
+ runs: 1 runs per test
7380
+ setup: <sh> | [<sh>, ...] once per eval, in this folder; what it writes to
7381
+ $FIREGOAT_OUT is first on the agent's PATH
7382
+ workdir_setup: <sh> | [...] in each fresh folder before the agent starts
7383
+ vars: {KEY: value} defaults for {{vars.KEY}} in prompts and checks;
7384
+ --var KEY=value overrides
7385
+ tests:
7386
+ - name: quickstart letters, digits, '.', '_', '-'; unique
7387
+ prompt: ... what a user would ask the agent; one concrete outcome
7388
+ end_goal: ... what is true when the problem is solved; not graded by
7389
+ firegoat, handed to you to judge
7390
+ checks: optional shell commands, run in the agent's folder
7391
+ - run: ... afterwards; exit 0 = pass; they set eval's exit code
7392
+ timeout: 600 optional, seconds
7393
+
7394
+ Unknown keys are errors. setup, workdir_setup and checks get the vars as env vars,
7395
+ plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; checks also get $FIREGOAT_SESSION (the
7396
+ claude session file, one JSON event per line).
7397
+
7398
+ Example:
7399
+ setup: npm pack --pack-destination "$FIREGOAT_OUT"
7400
+ workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
7401
+ tests:
7402
+ - name: first-call
7403
+ prompt: >-
7404
+ Using the acme SDK (already installed from the build under test), send a test
7405
+ event and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
7406
+ end_goal: >-
7407
+ A script sends one event with the SDK's client (not raw HTTP) and prints its
7408
+ id, without installing acme from the registry.
7409
+ checks:
7410
+ - run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_SESSION"
7411
+ - run: |
7412
+ ! grep -Eq 'npm (install|i) [^"]*acme' "$FIREGOAT_SESSION"
7413
+
7414
+ Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
7415
+ var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json]
7416
+ [--claude-bin PATH]
7417
+
7418
+ Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
7419
+ in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with setup:'s
7420
+ outputs on PATH. Then the test's checks run in that folder.
7421
+
7422
+ claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
7423
+ with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
7424
+ plugins, skills or MCP servers. claude saves its session there. There is no
7425
+ sandbox: the agent can reach the rest of your machine, including the repo.
7426
+
7427
+ test.. only these tests (by name)
7428
+ --runs N runs per test (overrides runs: in firegoat.yaml)
7429
+ --var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
7430
+ --json the summary as JSON on stdout (or {error, exitCode} if eval
7431
+ stops early); progress goes to stderr
7432
+ --claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
7433
+
7434
+ Output: .firegoat/runs/<runId>/
7435
+ summary.json every run, plus totals
7436
+ <test>/<n>/run.json one run: status and reason (from the checks), prompt,
7437
+ end_goal, checks [{run, code}], and paths: workdir,
7438
+ session (claude's session file), stderr, checksLog
7439
+ setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
7440
+
7441
+ Grading: firegoat runs only the script checks; they set the exit code. end_goal is
7442
+ yours to judge. The session is one JSON event per line: "assistant" messages hold
7443
+ "tool_use" items (the agent's commands), "user" messages their output.
7444
+
7445
+ Examples:
7446
+ npx firegoat eval --json all tests, JSON summary on stdout
7447
+ npx firegoat eval quickstart --runs 3 one test, three runs
7448
+ npx firegoat eval --var SHA=$(git rev-parse HEAD)
7449
+
7450
+ A run can take minutes (default timeout 600s per test).
7451
+
7452
+ ${EXIT_CODES}`;
7453
+
7878
7454
  // src/setup.ts
7879
- import { cpSync, createWriteStream as createWriteStream2, mkdirSync as mkdirSync4 } from "node:fs";
7880
- import { delimiter, join as join4 } from "node:path";
7881
- var SETUP_TIMEOUT_MS = 600000;
7882
- async function runAll(commands, cwd, env, logPath, what) {
7883
- const log = createWriteStream2(logPath);
7455
+ import { createWriteStream } from "node:fs";
7456
+ var STEP_TIMEOUT_MS = 600000;
7457
+ async function runSteps(opts) {
7458
+ const log = createWriteStream(opts.logPath);
7884
7459
  try {
7885
- for (const run of commands) {
7460
+ for (const run of opts.commands) {
7886
7461
  const r = await runShell({
7887
7462
  run,
7888
- cwd,
7889
- env,
7890
- timeoutMs: SETUP_TIMEOUT_MS,
7463
+ cwd: opts.cwd,
7464
+ env: opts.env,
7465
+ timeoutMs: STEP_TIMEOUT_MS,
7891
7466
  log
7892
7467
  });
7893
7468
  if (r.code !== 0)
7894
- return `${what} failed (${r.error ?? `exit ${r.code}`}): ${run} (log: ${logPath})`;
7469
+ return `${opts.what} failed (${r.error ?? `exit ${r.code}`}): ${run} (log: ${opts.logPath})`;
7895
7470
  }
7896
7471
  return;
7897
7472
  } finally {
7898
7473
  await new Promise((r) => log.end(r));
7899
7474
  }
7900
7475
  }
7901
- async function setupOnce(opts) {
7902
- mkdirSync4(opts.out, { recursive: true });
7903
- return runAll(opts.commands, opts.root, { ...opts.env, FIREGOAT_OUT: opts.out }, opts.logPath, "setup");
7904
- }
7905
- async function prepareRun(opts) {
7906
- const out = join4(opts.runRoot, "out");
7907
- if (opts.out === null)
7908
- mkdirSync4(out);
7909
- else
7910
- cpSync(opts.out, out, { recursive: true });
7911
- const path = [out, opts.env.PATH ?? ""].join(delimiter);
7912
- const error = await runAll(opts.commands, opts.workdir, {
7913
- ...opts.env,
7914
- PATH: path,
7915
- FIREGOAT_OUT: out,
7916
- FIREGOAT_WORKDIR: opts.workdir
7917
- }, opts.logPath, "workdir_setup");
7918
- return error ? { error } : { out, path };
7919
- }
7920
7476
 
7921
7477
  // src/which.ts
7922
7478
  import { accessSync, constants, statSync } from "node:fs";
7923
- import { delimiter as delimiter2, join as join5, resolve } from "node:path";
7479
+ import { delimiter, join as join2, resolve } from "node:path";
7924
7480
  function resolveBin(bin) {
7925
7481
  const executable = (p) => {
7926
7482
  try {
@@ -7932,39 +7488,15 @@ function resolveBin(bin) {
7932
7488
  };
7933
7489
  if (bin.includes("/"))
7934
7490
  return executable(bin) ? resolve(bin) : undefined;
7935
- for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
7936
- if (dir && executable(join5(dir, bin)))
7937
- return join5(dir, bin);
7491
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
7492
+ if (dir && executable(join2(dir, bin)))
7493
+ return join2(dir, bin);
7938
7494
  }
7939
7495
  return;
7940
7496
  }
7941
7497
 
7942
7498
  // src/eval.ts
7943
7499
  var CHECK_TIMEOUT_MS = 120000;
7944
- var GRADING_HINT = "firegoat ran only the script checks. Each run's end_goal is for the caller to judge, from its session (stream-json transcript) and workdir.";
7945
- function blankRun(test, n, dir, prompt) {
7946
- return {
7947
- test: test.name,
7948
- n,
7949
- status: "error",
7950
- reason: "",
7951
- prompt,
7952
- end_goal: test.end_goal,
7953
- checks: [],
7954
- dir,
7955
- workdir: null,
7956
- session: join6(dir, "session.jsonl"),
7957
- stderr: join6(dir, "stderr.txt"),
7958
- checksLog: join6(dir, "checks.txt"),
7959
- num_turns: null,
7960
- cost: null,
7961
- duration_ms: 0
7962
- };
7963
- }
7964
- function writeRunJson(r) {
7965
- writeFileSync2(join6(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
7966
- `);
7967
- }
7968
7500
 
7969
7501
  class UsageError extends Error {
7970
7502
  }
@@ -7972,7 +7504,6 @@ function parseArgs(args) {
7972
7504
  const opts = {
7973
7505
  names: [],
7974
7506
  json: false,
7975
- host: false,
7976
7507
  vars: {},
7977
7508
  claudeBin: process.env.FIREGOAT_CLAUDE_BIN || "claude"
7978
7509
  };
@@ -7991,8 +7522,6 @@ function parseArgs(args) {
7991
7522
  };
7992
7523
  if (flag === "--json")
7993
7524
  opts.json = true;
7994
- else if (flag === "--host")
7995
- opts.host = true;
7996
7525
  else if (flag === "--var") {
7997
7526
  const kv = value();
7998
7527
  const at = kv.indexOf("=");
@@ -8015,151 +7544,135 @@ function parseArgs(args) {
8015
7544
  }
8016
7545
  return opts;
8017
7546
  }
7547
+ function findSession(configDir, sessionId) {
7548
+ const projects = join3(configDir, "projects");
7549
+ if (!existsSync3(projects))
7550
+ return null;
7551
+ for (const d of readdirSync(projects)) {
7552
+ const f = join3(projects, d, `${sessionId}.jsonl`);
7553
+ if (existsSync3(f))
7554
+ return f;
7555
+ }
7556
+ return null;
7557
+ }
8018
7558
  function newRunId() {
8019
7559
  const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
8020
7560
  return `${ts}-${randomBytes(2).toString("hex")}`;
8021
7561
  }
8022
- function gitignoreHasFiregoat(cwd) {
8023
- const file = join6(cwd, ".gitignore");
8024
- if (!existsSync4(file))
8025
- return false;
8026
- return readFileSync3(file, "utf8").split(`
8027
- `).map((l) => l.trim()).some((l) => /^\/?\.firegoat\/?(\*\*?)?$/.test(l));
8028
- }
8029
- function newRunRoot(label) {
8030
- const runRoot = realpathSync2(mkdtempSync(join6(tmpdir(), `firegoat-${label}-`)));
8031
- const workdir = join6(runRoot, "work");
8032
- mkdirSync5(workdir);
8033
- return { runRoot, workdir };
8034
- }
8035
- function isolate(iso, claudeBin, runRoot, workdir) {
8036
- if (iso.host)
8037
- return { profile: null };
8038
- const profile = sandboxProfile({
8039
- claudeBin,
8040
- runRoot,
8041
- configDir: claudeConfigDir(),
8042
- repo: iso.repo
8043
- });
8044
- const broken = probeSandbox({ profile, workdir, repoFile: iso.repoFile });
8045
- return broken ? { error: broken } : { profile };
8046
- }
8047
7562
  async function runOne(opts) {
8048
7563
  const { test, n } = opts;
8049
7564
  const started = Date.now();
8050
- const dir = join6(opts.runDir, test.name, String(n));
8051
- mkdirSync5(dir, { recursive: true });
8052
- const summary = blankRun(test, n, dir, render(test.prompt, opts.vars));
8053
- const session = summary.session;
7565
+ const dir = join3(opts.runDir, test.name, String(n));
7566
+ const workdir = join3(dir, "work");
7567
+ mkdirSync2(workdir, { recursive: true });
7568
+ const r = {
7569
+ test: test.name,
7570
+ n,
7571
+ status: "error",
7572
+ reason: "",
7573
+ prompt: render(test.prompt, opts.vars),
7574
+ end_goal: test.end_goal,
7575
+ checks: [],
7576
+ dir,
7577
+ workdir,
7578
+ session: null,
7579
+ stderr: join3(dir, "stderr.txt"),
7580
+ checksLog: join3(dir, "checks.txt"),
7581
+ num_turns: null,
7582
+ cost: null,
7583
+ duration_ms: 0
7584
+ };
8054
7585
  const done = (status, reason) => {
8055
- summary.status = status;
8056
- summary.reason = reason;
8057
- summary.duration_ms = Date.now() - started;
8058
- writeRunJson(summary);
8059
- return summary;
7586
+ r.status = status;
7587
+ r.reason = reason;
7588
+ r.duration_ms = Date.now() - started;
7589
+ writeFileSync2(join3(dir, "run.json"), `${JSON.stringify(r, null, 2)}
7590
+ `);
7591
+ return r;
8060
7592
  };
8061
- const { runRoot, workdir } = newRunRoot(`${test.name}-${n}`);
8062
- summary.workdir = workdir;
8063
- const sandbox = isolate(opts.iso, opts.claudeBin, runRoot, workdir);
8064
- if ("error" in sandbox)
8065
- return done("error", `not graded: ${sandbox.error}`);
8066
- const hostEnv = { ...process.env, ...opts.vars };
8067
- let extra = {};
8068
- if (opts.out !== null || opts.workdirSetup.length > 0) {
8069
- const prepared = await prepareRun({
8070
- out: opts.out,
8071
- runRoot,
8072
- workdir,
7593
+ const stepEnv = { ...opts.stepEnv, FIREGOAT_WORKDIR: workdir };
7594
+ if (opts.workdirSetup.length > 0) {
7595
+ const error = await runSteps({
7596
+ what: "workdir_setup",
8073
7597
  commands: opts.workdirSetup,
8074
- env: hostEnv,
8075
- logPath: join6(dir, "workdir_setup.txt")
7598
+ cwd: workdir,
7599
+ env: stepEnv,
7600
+ logPath: join3(dir, "workdir_setup.txt")
8076
7601
  });
8077
- if ("error" in prepared)
8078
- return done("error", `not graded: ${prepared.error}`);
8079
- if (opts.out !== null)
8080
- extra = { PATH: prepared.path, FIREGOAT_OUT: prepared.out };
7602
+ if (error)
7603
+ return done("error", `not graded: ${error}`);
8081
7604
  }
8082
- const agentPath = extra.PATH ? { PATH: extra.PATH } : {};
7605
+ const stderr = createWriteStream2(r.stderr);
8083
7606
  const outcome = await runClaude({
8084
- env: {
8085
- ...agentEnv(runRoot, opts.auth.method === "api-key" ? opts.auth.apiKey : undefined),
8086
- ...agentPath
8087
- },
8088
- keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
8089
- profile: sandbox.profile,
8090
7607
  bin: opts.claudeBin,
8091
- prompt: summary.prompt,
7608
+ prompt: r.prompt,
8092
7609
  cwd: workdir,
8093
- sessionPath: session,
8094
- stderrPath: summary.stderr,
7610
+ env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
7611
+ stderr,
8095
7612
  timeoutMs: test.timeout * 1000
8096
7613
  });
8097
- if (outcome.result) {
8098
- summary.num_turns = outcome.result.num_turns ?? null;
8099
- summary.cost = outcome.result.total_cost_usd ?? null;
8100
- }
7614
+ await new Promise((res) => stderr.end(res));
7615
+ if (outcome.sessionId)
7616
+ r.session = findSession(opts.configDir, outcome.sessionId);
7617
+ r.num_turns = outcome.result?.num_turns ?? null;
7618
+ r.cost = outcome.result?.total_cost_usd ?? null;
8101
7619
  if (!outcome.ok)
8102
7620
  return done("error", `not graded: ${outcome.reason}`);
8103
- if (test.checks.length === 0)
8104
- return done("pass", "no script checks; end_goal is for the caller to judge");
8105
- const log = createWriteStream3(summary.checksLog);
8106
- const env = {
8107
- ...process.env,
8108
- ...hostEnv,
8109
- ...extra,
8110
- FIREGOAT_SESSION: resolve2(session),
8111
- FIREGOAT_WORKDIR: workdir
8112
- };
8113
- const results = [];
7621
+ if (!r.session)
7622
+ return done("error", `not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`);
7623
+ const log = createWriteStream2(r.checksLog);
8114
7624
  try {
8115
- for (const c of test.checks) {
8116
- results.push(await runShell({
7625
+ for (const c of test.checks)
7626
+ r.checks.push(await runShell({
8117
7627
  run: render(c.run, opts.vars),
8118
7628
  cwd: workdir,
8119
- env,
7629
+ env: { ...stepEnv, FIREGOAT_SESSION: r.session },
8120
7630
  timeoutMs: CHECK_TIMEOUT_MS,
8121
7631
  log
8122
7632
  }));
8123
- }
8124
7633
  } finally {
8125
- await new Promise((r) => log.end(r));
7634
+ await new Promise((res) => log.end(res));
8126
7635
  }
8127
- summary.checks = results.map((r) => r.error === undefined ? { run: r.run, code: r.code } : { run: r.run, code: r.code, error: r.error });
8128
- const broken = results.find((r) => r.notStarted);
8129
- if (broken)
8130
- return done("error", `not graded: check ${broken.run}: ${broken.error}`);
8131
- const failed = results.filter((r) => r.code !== 0);
7636
+ const failed = r.checks.filter((c) => c.code !== 0);
8132
7637
  if (failed.length > 0)
8133
- return done("fail", failed.map((r) => `check failed (${r.error ?? `exit ${r.code}`}): ${r.run}`).join("; "));
8134
- return done("pass", `${results.length}/${results.length} checks passed`);
7638
+ return done("fail", failed.map((c) => `check failed (${c.error ?? `exit ${c.code}`}): ${c.run}`).join("; "));
7639
+ return done("pass", test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`);
8135
7640
  }
8136
- async function evalCommand(args, cwd) {
7641
+ async function evalCommand(args, root) {
8137
7642
  if (args.includes("--help") || args.includes("-h")) {
8138
7643
  console.log(EVAL_HELP);
8139
7644
  return EXIT.pass;
8140
7645
  }
7646
+ const json = args.includes("--json");
7647
+ const fatal = (exitCode, error) => {
7648
+ console.error(`firegoat eval: ${error}`);
7649
+ if (json)
7650
+ console.log(JSON.stringify({ error, exitCode }));
7651
+ return exitCode;
7652
+ };
8141
7653
  let opts;
8142
7654
  try {
8143
7655
  opts = parseArgs(args);
8144
7656
  } catch (e) {
8145
- if (e instanceof UsageError) {
8146
- console.error(`firegoat eval: ${e.message}`);
8147
- return EXIT.usage;
8148
- }
7657
+ if (e instanceof UsageError)
7658
+ return fatal(EXIT.usage, e.message);
8149
7659
  throw e;
8150
7660
  }
8151
7661
  const say = opts.json ? console.error : console.log;
8152
- const warn = console.error;
8153
- const root = findConfigDir(cwd);
8154
- const config = loadConfig(root, warn);
7662
+ let config;
7663
+ try {
7664
+ config = loadConfig(root);
7665
+ } catch (e) {
7666
+ if (e instanceof ConfigError)
7667
+ return fatal(EXIT.usage, e.message);
7668
+ throw e;
7669
+ }
8155
7670
  let tests = config.tests;
8156
7671
  if (opts.names.length > 0) {
8157
7672
  const known = new Set(tests.map((t) => t.name));
8158
7673
  const unknown = opts.names.filter((n) => !known.has(n));
8159
- if (unknown.length > 0) {
8160
- console.error(`firegoat eval: unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
8161
- return EXIT.usage;
8162
- }
7674
+ if (unknown.length > 0)
7675
+ return fatal(EXIT.usage, `unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
8163
7676
  tests = tests.filter((t) => opts.names.includes(t.name));
8164
7677
  }
8165
7678
  const runs = opts.runs ?? config.runs;
@@ -8167,572 +7680,205 @@ async function evalCommand(args, cwd) {
8167
7680
  const missing = [
8168
7681
  ...new Set(tests.flatMap((t) => [t.prompt, ...t.checks.map((c) => c.run)]).flatMap(varRefs).filter((k) => !(k in vars)))
8169
7682
  ];
8170
- if (missing.length > 0) {
8171
- console.error(`firegoat eval: no value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`);
8172
- return EXIT.usage;
8173
- }
7683
+ if (missing.length > 0)
7684
+ return fatal(EXIT.usage, `no value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`);
8174
7685
  const claudePath = resolveBin(opts.claudeBin);
8175
- if (!claudePath) {
8176
- console.error(`firegoat eval: not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH / FIREGOAT_CLAUDE_BIN.`);
8177
- return EXIT.notGraded;
8178
- }
8179
- if (opts.host)
8180
- warn("warning: --host: the agent runs without isolation and can read and write your files");
8181
- else if (process.platform !== "darwin" || !existsSync4(SANDBOX_EXEC)) {
8182
- console.error(`firegoat eval: isolation needs macOS (${SANDBOX_EXEC}); this is ${process.platform}. In CI or Linux, run inside a throwaway container and pass --host.`);
8183
- return EXIT.usage;
8184
- }
8185
- const iso = {
8186
- host: opts.host,
8187
- repo: root,
8188
- repoFile: join6(root, "firegoat.yaml")
8189
- };
8190
- let auth;
8191
- try {
8192
- auth = resolveAuth(process.env);
8193
- } catch (e) {
8194
- if (e instanceof AuthError) {
8195
- console.error(`firegoat eval: ${e.message}`);
8196
- return EXIT.usage;
8197
- }
8198
- throw e;
8199
- }
8200
- say(`auth: ${auth.label}`);
8201
- if (auth.method === "claude" && process.env.ANTHROPIC_API_KEY)
8202
- warn("note: ANTHROPIC_API_KEY in your shell is not passed to the agent (it is used only in CI, or after `firegoat init --auth api-key`)");
8203
- mkdirSync5(claudeConfigDir(), { recursive: true });
8204
- const pre = newRunRoot("preflight");
8205
- const preSandbox = isolate(iso, claudePath, pre.runRoot, pre.workdir);
8206
- const loginProblem = "error" in preSandbox ? preSandbox.error : auth.method === "claude" ? checkLogin({
7686
+ if (!claudePath)
7687
+ return fatal(EXIT.notGraded, `not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH / FIREGOAT_CLAUDE_BIN.`);
7688
+ const firegoatDir = ensureFiregoatDir(root);
7689
+ const configDir = join3(firegoatDir, ".claude");
7690
+ mkdirSync2(configDir, { recursive: true });
7691
+ const loginProblem = checkLogin({
8207
7692
  bin: claudePath,
8208
- env: agentEnv(pre.runRoot),
8209
- profile: preSandbox.profile,
8210
- cwd: pre.workdir
8211
- }) : undefined;
8212
- if (loginProblem) {
8213
- console.error(`firegoat eval: not graded: ${loginProblem}`);
8214
- return EXIT.notGraded;
8215
- }
8216
- mkdirSync5(join6(root, ".firegoat"), { recursive: true });
8217
- if (!gitignoreHasFiregoat(root))
8218
- warn("hint: add .firegoat/ to .gitignore (it holds run traces)");
7693
+ env: claudeEnv(configDir),
7694
+ cwd: root
7695
+ });
7696
+ if (loginProblem)
7697
+ return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
8219
7698
  const runId = newRunId();
8220
- const runDir = join6(root, ".firegoat", "runs", runId);
8221
- mkdirSync5(runDir, { recursive: true });
8222
- const summaryPath = join6(runDir, "summary.json");
8223
- let out = null;
7699
+ const runDir = join3(firegoatDir, "runs", runId);
7700
+ const out = join3(runDir, "out");
7701
+ mkdirSync2(out, { recursive: true });
7702
+ const stepEnv = {
7703
+ ...process.env,
7704
+ ...vars,
7705
+ FIREGOAT_OUT: out,
7706
+ PATH: [out, process.env.PATH ?? ""].join(delimiter2)
7707
+ };
8224
7708
  if (config.setup.length > 0) {
8225
7709
  say("setup...");
8226
- out = join6(runDir, "out");
8227
- const error = await setupOnce({
7710
+ const error = await runSteps({
7711
+ what: "setup",
8228
7712
  commands: config.setup,
8229
- root,
8230
- out,
8231
- env: { ...process.env, ...vars },
8232
- logPath: join6(runDir, "setup.txt")
7713
+ cwd: root,
7714
+ env: stepEnv,
7715
+ logPath: join3(runDir, "setup.txt")
8233
7716
  });
8234
- if (error) {
8235
- console.error(`firegoat eval: not graded: ${error}`);
8236
- return EXIT.notGraded;
8237
- }
7717
+ if (error)
7718
+ return fatal(EXIT.notGraded, `not graded: ${error}`);
8238
7719
  }
8239
7720
  const results = [];
8240
- const writeSummary = () => {
8241
- const totals = { pass: 0, fail: 0, error: 0 };
8242
- for (const r of results)
8243
- totals[r.status]++;
8244
- const doc = {
8245
- runId,
8246
- runsDir: runDir,
8247
- isolation: opts.host ? "host" : "seatbelt",
8248
- auth: auth.method,
8249
- grading: GRADING_HINT,
8250
- totals,
8251
- runs: results
8252
- };
8253
- writeFileSync2(summaryPath, `${JSON.stringify(doc, null, 2)}
8254
- `);
8255
- return doc;
8256
- };
8257
- for (const test of tests) {
7721
+ for (const test of tests)
8258
7722
  for (let n = 1;n <= runs; n++) {
8259
- let r;
8260
- try {
8261
- r = await runOne({
8262
- test,
8263
- n,
8264
- runDir,
8265
- claudeBin: claudePath,
8266
- iso,
8267
- auth,
8268
- out,
8269
- workdirSetup: config.workdirSetup,
8270
- vars,
8271
- warn
8272
- });
8273
- } catch (e) {
8274
- const dir = join6(runDir, test.name, String(n));
8275
- mkdirSync5(dir, { recursive: true });
8276
- r = blankRun(test, n, dir, test.prompt);
8277
- r.reason = `not graded: firegoat crashed: ${e.message}`;
8278
- writeRunJson(r);
8279
- }
7723
+ const r = await runOne({
7724
+ test,
7725
+ n,
7726
+ runDir,
7727
+ claudeBin: claudePath,
7728
+ configDir,
7729
+ workdirSetup: config.workdirSetup,
7730
+ stepEnv,
7731
+ vars
7732
+ });
8280
7733
  results.push(r);
8281
- writeSummary();
8282
7734
  say(`${r.status.toUpperCase()} ${r.test}#${r.n} ${r.reason}`);
8283
7735
  }
8284
- }
8285
- const doc = writeSummary();
8286
- const { pass, fail, error } = doc.totals;
7736
+ const totals = { pass: 0, fail: 0, error: 0 };
7737
+ for (const r of results)
7738
+ totals[r.status]++;
7739
+ const doc = { runId, runsDir: runDir, totals, runs: results };
7740
+ writeFileSync2(join3(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
7741
+ `);
8287
7742
  say(`
8288
- ${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
8289
- say(`runs: ${runDir}`);
8290
- say(`end_goal is not graded by firegoat; per run: ${join6(runDir, "<test>", "<n>", "run.json")}`);
7743
+ ${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${results.length} runs)`);
7744
+ say(`runs: ${runDir} (end_goal is for you to judge: see run.json per run)`);
8291
7745
  if (opts.json)
8292
7746
  console.log(JSON.stringify(doc, null, 2));
8293
- return error > 0 ? EXIT.notGraded : fail > 0 ? EXIT.fail : EXIT.pass;
7747
+ return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
8294
7748
  }
8295
7749
 
8296
7750
  // src/init.ts
8297
- import { spawnSync as spawnSync4 } from "node:child_process";
8298
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
8299
- import { basename as basename2, join as join7 } from "node:path";
8300
- import { createInterface as createInterface2 } from "node:readline/promises";
8301
- import { Writable } from "node:stream";
8302
- var SKILLS = ["firegoat-onboarding", "suggesting-eval-prompts"];
7751
+ import { spawnSync as spawnSync2 } from "node:child_process";
7752
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "node:fs";
7753
+ import { basename, join as join4 } from "node:path";
7754
+ import { createInterface } from "node:readline/promises";
8303
7755
  var SKILLS_ARGV = [
8304
7756
  "skills",
8305
7757
  "add",
8306
- "team2027/skills",
8307
- ...SKILLS.flatMap((s) => ["--skill", s]),
7758
+ "firegoat-ai/skills",
7759
+ "--skill",
7760
+ "firegoat-onboarding",
7761
+ "--skill",
7762
+ "suggesting-eval-prompts",
8308
7763
  "--agent",
8309
7764
  "claude-code",
7765
+ "--copy",
8310
7766
  "-y"
8311
7767
  ];
8312
- var GITIGNORE_LINE = ".firegoat/";
8313
- function readText(file) {
8314
- try {
8315
- return readFileSync4(file, "utf8");
8316
- } catch {
8317
- return;
8318
- }
8319
- }
8320
- function tomlSections(text) {
8321
- const sections = new Map;
8322
- let current = "";
8323
- sections.set(current, new Map);
8324
- for (const raw of text.split(`
8325
- `)) {
8326
- const line = raw.trim();
8327
- const header = line.match(/^\[\[?\s*([^\]]+?)\s*\]\]?$/);
8328
- if (header?.[1]) {
8329
- current = header[1];
8330
- if (!sections.has(current))
8331
- sections.set(current, new Map);
8332
- continue;
8333
- }
8334
- const kv = line.match(/^([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*["']([^"']*)["']/);
8335
- if (kv?.[1] && kv[2] !== undefined)
8336
- sections.get(current)?.set(kv[1].replace(/"/g, ""), kv[2]);
8337
- }
8338
- return sections;
8339
- }
8340
- function detect(cwd) {
8341
- const readme = ["README.md", "README", "readme.md", "Readme.md"].find((f) => existsSync5(join7(cwd, f)));
8342
- const fallback = { name: basename2(cwd), library: true, readme };
8343
- const pkgText = readText(join7(cwd, "package.json"));
8344
- if (pkgText !== undefined) {
8345
- try {
8346
- const pkg = JSON.parse(pkgText);
8347
- const name = typeof pkg.name === "string" ? pkg.name : fallback.name;
8348
- let bin;
8349
- if (typeof pkg.bin === "string")
8350
- bin = name.replace(/^@[^/]+\//, "");
8351
- else if (pkg.bin && typeof pkg.bin === "object")
8352
- bin = Object.keys(pkg.bin)[0];
8353
- const library = !bin || pkg.main !== undefined || pkg.exports !== undefined || pkg.module !== undefined;
8354
- const docsUrl = typeof pkg.homepage === "string" && /^https?:\/\//.test(pkg.homepage) ? pkg.homepage : undefined;
8355
- return { ecosystem: "npm", name, bin, library, readme, docsUrl };
8356
- } catch {}
8357
- }
8358
- const pyText = readText(join7(cwd, "pyproject.toml"));
8359
- if (pyText !== undefined) {
8360
- const s = tomlSections(pyText);
8361
- const name = s.get("project")?.get("name") ?? s.get("tool.poetry")?.get("name") ?? fallback.name;
8362
- const scripts = s.get("project.scripts") ?? s.get("tool.poetry.scripts");
8363
- const bin = scripts ? [...scripts.keys()][0] : undefined;
8364
- const urls = s.get("project.urls");
8365
- const docsUrl = urls?.get("Documentation") ?? urls?.get("documentation") ?? urls?.get("Homepage") ?? urls?.get("homepage");
8366
- return { ecosystem: "python", name, bin, library: true, readme, docsUrl };
8367
- }
8368
- const goText = readText(join7(cwd, "go.mod"));
8369
- if (goText !== undefined) {
8370
- const name = goText.match(/^module\s+(\S+)/m)?.[1] ?? fallback.name;
8371
- return { ecosystem: "go", name, library: true, readme };
8372
- }
8373
- const cargoText = readText(join7(cwd, "Cargo.toml"));
8374
- if (cargoText !== undefined) {
8375
- const name = tomlSections(cargoText).get("package")?.get("name");
8376
- return {
8377
- ecosystem: "rust",
8378
- name: name ?? fallback.name,
8379
- library: true,
8380
- readme
8381
- };
8382
- }
8383
- return fallback;
8384
- }
8385
- var LANG = {
8386
- npm: "Node.js",
8387
- python: "Python",
8388
- go: "Go",
8389
- rust: "Rust"
8390
- };
8391
- function importName(p) {
8392
- if (p.ecosystem === "python" || p.ecosystem === "rust")
8393
- return p.name.replace(/-/g, "_");
8394
- return p.name;
8395
- }
8396
- var GREP_EXCLUDES = [
8397
- "--exclude=output.txt",
8398
- "--exclude=package.json",
8399
- "--exclude=package-lock.json",
8400
- "--exclude=pyproject.toml",
8401
- "--exclude=requirements.txt",
8402
- "--exclude=go.mod",
8403
- "--exclude=go.sum",
8404
- "--exclude=Cargo.toml",
8405
- "--exclude=Cargo.lock",
8406
- "--exclude-dir=node_modules",
8407
- "--exclude-dir=.venv",
8408
- "--exclude-dir=target"
8409
- ].join(" ");
8410
- function sq(s) {
8411
- return `'${s.replace(/'/g, `'\\''`)}'`;
8412
- }
8413
- function templates(p) {
8414
- const docs = p.docsUrl ? `its docs at ${p.docsUrl}` : "its official docs";
8415
- const out = [];
8416
- if (p.bin) {
8417
- out.push({
8418
- name: "cli-first-run",
8419
- prompt: `I just heard about the \`${p.bin}\` command-line tool from ${p.name} and want to see it actually do something useful. Use it for one basic real task and save what it prints to cli-output.txt. Keep it minimal. Use only ${docs} as a guide.`,
8420
- end_goal: `cli-output.txt exists and holds real output from running \`${p.bin}\`, not an install log or an error.`,
8421
- checks: [
8422
- "test -s cli-output.txt",
8423
- "! grep -qiE 'command not found|not recognized|ENOENT' cli-output.txt"
8424
- ]
8425
- });
8426
- }
8427
- if (p.library && p.ecosystem) {
8428
- out.push({
8429
- name: "quickstart",
8430
- prompt: `I want to use ${p.name} in a new ${LANG[p.ecosystem]} project. Get the smallest working example going: a program that uses ${p.name} to do one real thing, then run it and save what it prints to output.txt. Keep it minimal. Use only ${docs} as a guide.`,
8431
- end_goal: `A program in the workdir uses ${p.name}, runs without errors, and its output is saved in output.txt.`,
8432
- checks: [
8433
- "test -s output.txt",
8434
- `grep -rqsF ${GREP_EXCLUDES} -- ${sq(importName(p))} .`
8435
- ]
8436
- });
8437
- }
8438
- if (out.length === 0) {
8439
- out.push({
8440
- name: "quickstart",
8441
- prompt: `I want to try ${p.name}. Follow its quickstart in a new project, get the smallest example working, then run it and save what it prints to output.txt. Keep it minimal. Use only ${docs} as a guide.`,
8442
- end_goal: `The ${p.name} quickstart example runs without errors and its output is saved in output.txt.`,
8443
- checks: ["test -s output.txt"]
8444
- });
8445
- }
8446
- return out;
8447
- }
8448
- function block(text, indent) {
8449
- return text.split(`
8450
- `).map((l) => indent + l).join(`
8451
- `);
8452
- }
8453
- function renderYaml(p, tests) {
8454
- const lines = [
8455
- `# firegoat.yaml: evals for how well coding agents use ${p.name}.`,
8456
- "# Each test runs a coding agent in an empty workdir with `prompt` as the first message.",
8457
- "# These are starter templates: edit the prompts to match real user problems.",
8458
- "# Good prompts state one concrete outcome as a user would, not implementation steps,",
8459
- "# and ask for observable proof (stdout or a file) that `checks` can verify.",
8460
- "",
8461
- "# Agents to evaluate (v1 supports: claude-code).",
8462
- "agents: [claude-code]",
8463
- "# How many times to run each test.",
8464
- "runs: 1",
8465
- "",
8466
- "# Optional: test your local build instead of the published package.",
8467
- "# setup: runs once per eval, in this folder, on your machine. Whatever it writes to",
8468
- "# $FIREGOAT_OUT is copied into each run's folder and put on the agent's PATH.",
8469
- "# workdir_setup: runs in each fresh workdir before the agent starts. Then tell the",
8470
- '# agent in the prompt: "The package is already installed from the build under test."',
8471
- "#",
8472
- "# npm:",
8473
- '# setup: npm pack --pack-destination "$FIREGOAT_OUT"',
8474
- '# workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz',
8475
- "#",
8476
- "# python:",
8477
- '# setup: uv build --wheel --out-dir "$FIREGOAT_OUT"',
8478
- '# workdir_setup: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl',
8479
- "#",
8480
- "# a CLI binary (lands on PATH as is):",
8481
- '# setup: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli',
8482
- "#",
8483
- "# Values for {{vars.KEY}} in prompts and checks, overridden by `--var KEY=value`;",
8484
- "# setup commands and checks also get them as env vars.",
8485
- "# vars:",
8486
- "# SHA: main",
8487
- "",
8488
- "tests:"
8489
- ];
8490
- for (const t of tests) {
8491
- lines.push(` - name: ${t.name} # letters, digits, '.', '_' and '-'`, " # What a user would ask the agent. Edit this.", " prompt: >-", block(t.prompt, " "), " # One sentence that is true when the user's problem is solved. Not graded by", " # firegoat: it goes to your agent (eval --json, run.json) to judge.", " end_goal: >-", block(t.end_goal, " "), " # Shell commands run in the agent's workdir afterwards; exit 0 = pass.", " checks:", ...t.checks.map((c) => ` - run: ${JSON.stringify(c)}`), " # Seconds before the agent run is stopped.", " timeout: 600");
8492
- }
8493
- return `${lines.join(`
8494
- `)}
7768
+ function template(product) {
7769
+ return `# firegoat.yaml: evals for how well coding agents use ${product}.
7770
+ # Each test runs your claude in an empty folder (.firegoat/runs/...) with \`prompt\`
7771
+ # as the first message, then runs \`checks\` there. Reference: npx firegoat init --help
7772
+
7773
+ # How many times to run each test.
7774
+ runs: 1
7775
+
7776
+ # Optional: test your local build instead of the published package.
7777
+ # setup: runs once per eval, here; what it writes to $FIREGOAT_OUT is put on the
7778
+ # agent's PATH. workdir_setup: runs in each fresh folder before the agent starts.
7779
+ #
7780
+ # npm:
7781
+ # setup: npm pack --pack-destination "$FIREGOAT_OUT"
7782
+ # workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
7783
+ #
7784
+ # python:
7785
+ # setup: uv build --wheel --out-dir "$FIREGOAT_OUT"
7786
+ # workdir_setup: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl
7787
+ #
7788
+ # a CLI binary:
7789
+ # setup: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli
7790
+
7791
+ tests:
7792
+ - name: quickstart
7793
+ # What a user would ask their agent. Edit this.
7794
+ prompt: >-
7795
+ I want to try ${product}. Get the smallest working example going in a new
7796
+ project, run it, and save what it prints to output.txt. Keep it minimal.
7797
+ # True when the user's problem is solved. Not graded by firegoat: it goes to
7798
+ # your agent (eval --json, run.json) to judge.
7799
+ end_goal: >-
7800
+ A minimal project uses ${product}, runs without errors, and output.txt holds
7801
+ its output.
7802
+ # Shell commands run in the agent's folder afterwards; exit 0 = pass.
7803
+ checks:
7804
+ - run: test -s output.txt
7805
+ # Seconds before the agent run is stopped.
7806
+ timeout: 600
8495
7807
  `;
8496
7808
  }
8497
- function ensureGitignore(cwd) {
8498
- const file = join7(cwd, ".gitignore");
8499
- const text = readText(file);
8500
- if (text === undefined) {
8501
- writeFileSync3(file, `${GITIGNORE_LINE}
8502
- `);
8503
- return `created .gitignore with ${GITIGNORE_LINE}`;
8504
- }
8505
- const has = text.split(/\r?\n/).some((l) => ["/.firegoat/", ".firegoat/", ".firegoat"].includes(l.trim()));
8506
- if (has)
8507
- return `.gitignore already ignores ${GITIGNORE_LINE}`;
8508
- const sep = text === "" || text.endsWith(`
8509
- `) ? "" : `
8510
- `;
8511
- writeFileSync3(file, `${text}${sep}${GITIGNORE_LINE}
8512
- `);
8513
- return `added ${GITIGNORE_LINE} to .gitignore`;
8514
- }
8515
- function installSkills(cwd) {
8516
- const r = spawnSync4("npx", ["-y", ...SKILLS_ARGV], {
8517
- cwd,
8518
- stdio: "inherit"
8519
- });
8520
- if (r.error)
8521
- return r.error.message;
8522
- if (r.status !== 0)
8523
- return `exit ${r.status ?? "by signal"}`;
8524
- return;
8525
- }
8526
- async function ask(question) {
8527
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
8528
- try {
8529
- return (await rl.question(question)).trim();
8530
- } finally {
8531
- rl.close();
8532
- }
8533
- }
8534
- async function readSecret(question) {
8535
- if (!process.stdin.isTTY) {
8536
- const chunks = [];
8537
- for await (const c of process.stdin)
8538
- chunks.push(c);
8539
- return Buffer.concat(chunks).toString("utf8").trim();
8540
- }
8541
- process.stdout.write(question);
8542
- const silent = new Writable({ write: (_c, _e, cb) => cb() });
8543
- const rl = createInterface2({
8544
- input: process.stdin,
8545
- output: silent,
8546
- terminal: true
7809
+ var AGENT_PROMPT = "Scan my project and setup 2-3 agent experience prompts to test general usage of the app. Use firegoat skills";
7810
+ var CLIPBOARD = [
7811
+ ["pbcopy", []],
7812
+ ["wl-copy", []],
7813
+ ["xclip", ["-selection", "clipboard"]],
7814
+ ["clip", []]
7815
+ ];
7816
+ function copyToClipboard(text) {
7817
+ return CLIPBOARD.some(([cmd, args]) => {
7818
+ const r = spawnSync2(cmd, args, { input: text });
7819
+ return !r.error && r.status === 0;
8547
7820
  });
8548
- try {
8549
- return (await rl.question("")).trim();
8550
- } finally {
8551
- rl.close();
8552
- process.stdout.write(`
8553
- `);
8554
- }
8555
7821
  }
8556
- async function chooseAuth() {
8557
- let current;
7822
+ async function askYes(question) {
7823
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8558
7824
  try {
8559
- current = resolveAuth({}).label;
8560
- } catch (e) {
8561
- current = e.message;
8562
- }
8563
- console.log(`How should the agent sign in? (now: ${current})
8564
- 1) Login with Claude (your Claude plan)
8565
- 2) Paste an Anthropic API key`);
8566
- for (;; ) {
8567
- const a = await ask("choose [1/2]: ");
8568
- if (a === "1")
8569
- return "claude";
8570
- if (a === "2")
8571
- return "api-key";
8572
- console.log("type 1 or 2");
8573
- }
8574
- }
8575
- async function setUpAuth(method) {
8576
- if (method === "claude") {
8577
- const code = claudeLoginCommand([]);
8578
- return code === 0 ? undefined : code;
8579
- }
8580
- const key = await readSecret("Anthropic API key (input hidden): ");
8581
- if (!looksLikeAnthropicKey(key)) {
8582
- console.error("firegoat init: that does not look like an Anthropic API key (sk-ant-...); nothing stored");
8583
- return EXIT.usage;
8584
- }
8585
- const file = saveApiKey(key);
8586
- console.log(`stored the API key in ${file} (mode 0600, not in the repo); evals pass it to the agent as ANTHROPIC_API_KEY`);
8587
- return;
8588
- }
8589
- async function askYesNo(question) {
8590
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
8591
- try {
8592
- const answer = await rl.question(`${question} [y/N] `);
8593
- return /^y(es)?$/i.test(answer.trim());
7825
+ const a = (await rl.question(`${question} [Y/n] `)).trim();
7826
+ return a === "" || /^y(es)?$/i.test(a);
8594
7827
  } finally {
8595
7828
  rl.close();
8596
7829
  }
8597
7830
  }
8598
- function describe(p) {
8599
- if (!p.ecosystem)
8600
- return "no package manifest found, writing a generic test";
8601
- const parts = [`${p.ecosystem} package ${p.name}`];
8602
- if (p.bin)
8603
- parts.push(`bin: ${p.bin}`);
8604
- if (p.readme)
8605
- parts.push(`docs: ${p.readme}`);
8606
- return parts.join(", ");
8607
- }
8608
7831
  async function initCommand(args, cwd) {
8609
- let yes = false;
8610
7832
  let force = false;
8611
- let skills = false;
8612
- let auth;
8613
- for (let i = 0;i < args.length; i++) {
8614
- const a = args[i];
8615
- if (a === "--auth" || a.startsWith("--auth=")) {
8616
- const v = a === "--auth" ? args[++i] : a.slice(7);
8617
- if (v !== "claude" && v !== "api-key") {
8618
- console.error(`firegoat init: --auth must be claude or api-key, got ${v ?? "nothing"}`);
8619
- return EXIT.usage;
8620
- }
8621
- auth = v;
8622
- continue;
8623
- }
8624
- switch (a) {
8625
- case "--yes":
8626
- case "-y":
8627
- yes = true;
8628
- break;
8629
- case "--force":
8630
- case "-f":
8631
- force = true;
8632
- break;
8633
- case "--skills":
8634
- skills = true;
8635
- break;
8636
- case "--help":
8637
- case "-h":
8638
- console.log(INIT_HELP);
8639
- return 0;
8640
- default:
8641
- console.error(`firegoat init: unknown argument: ${a}
7833
+ let skills = true;
7834
+ for (const a of args) {
7835
+ if (a === "--help" || a === "-h") {
7836
+ console.log(INIT_HELP);
7837
+ return EXIT.pass;
7838
+ }
7839
+ if (a === "--force" || a === "-f")
7840
+ force = true;
7841
+ else if (a === "--no-skills")
7842
+ skills = false;
7843
+ else {
7844
+ console.error(`firegoat init: unknown argument: ${a}
8642
7845
 
8643
7846
  ${INIT_HELP}`);
8644
- return EXIT.usage;
8645
- }
8646
- }
8647
- const configPath = join7(cwd, CONFIG_FILE);
8648
- const keepConfig = existsSync5(configPath) && !force;
8649
- if (keepConfig && !skills && !auth) {
8650
- console.error(`firegoat init: ${CONFIG_FILE} already exists; not overwriting. Use --force to replace it.`);
8651
- return EXIT.usage;
8652
- }
8653
- if (keepConfig) {
8654
- console.log(`kept existing ${CONFIG_FILE} (use --force to replace it)`);
8655
- } else {
8656
- const project = detect(cwd);
8657
- const tests = templates(project);
8658
- console.log(`detected: ${describe(project)}`);
8659
- writeFileSync3(configPath, renderYaml(project, tests));
8660
- console.log(`wrote ${CONFIG_FILE} (${tests.length} test${tests.length === 1 ? "" : "s"}: ${tests.map((t) => t.name).join(", ")}); edit the prompts to fit your users`);
8661
- }
8662
- console.log(ensureGitignore(cwd));
8663
- const interactive = !yes && process.stdin.isTTY && process.stdout.isTTY;
8664
- if (!auth && interactive)
8665
- auth = await chooseAuth();
8666
- if (auth) {
8667
- const failed = await setUpAuth(auth);
8668
- if (failed !== undefined)
8669
- return failed;
8670
- }
8671
- if (!skills && interactive)
8672
- skills = await askYesNo(`Add the ${SKILLS.join(" and ")} skills to .claude/skills/ (help your agent draft better evals)?`);
7847
+ return EXIT.usage;
7848
+ }
7849
+ }
7850
+ ensureFiregoatDir(cwd);
7851
+ const configPath = join4(cwd, CONFIG_FILE);
7852
+ if (existsSync4(configPath) && !force)
7853
+ console.log(`kept ${CONFIG_FILE} (--force replaces it)`);
7854
+ else {
7855
+ writeFileSync3(configPath, template(basename(cwd)));
7856
+ console.log(`wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
7857
+ }
7858
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
7859
+ if (skills && interactive)
7860
+ skills = await askYes("Install the firegoat-onboarding skill for your coding agent?");
8673
7861
  if (skills) {
8674
7862
  console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
8675
- const failed = installSkills(cwd);
8676
- if (failed) {
8677
- console.error(`firegoat init: installing the skills failed (${failed}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
7863
+ const env = { ...process.env };
7864
+ delete env.npm_config_package;
7865
+ const r = spawnSync2("npx", ["-y", ...SKILLS_ARGV], {
7866
+ cwd,
7867
+ env,
7868
+ stdio: "inherit"
7869
+ });
7870
+ if (r.error || r.status !== 0) {
7871
+ console.error(`firegoat init: installing the skills failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
8678
7872
  return EXIT.notGraded;
8679
7873
  }
8680
- } else {
8681
- console.log(`hint: \`npx firegoat init --skills\` adds the ${SKILLS.join(" and ")} skills to .claude/skills/ (keeps your ${CONFIG_FILE})`);
8682
7874
  }
8683
- console.log("next: npx firegoat eval");
8684
- return 0;
8685
- }
7875
+ console.log(`
7876
+ Paste this into your coding agent:
8686
7877
 
8687
- // src/open.ts
8688
- import { spawnSync as spawnSync5 } from "node:child_process";
8689
- import { existsSync as existsSync6, readdirSync } from "node:fs";
8690
- import { join as join8 } from "node:path";
8691
- var OPENER = {
8692
- darwin: "open",
8693
- linux: "xdg-open"
8694
- };
8695
- function openCommand(args, cwd) {
8696
- let print = false;
8697
- const ids = [];
8698
- for (const a of args) {
8699
- if (a === "--help" || a === "-h") {
8700
- console.log(OPEN_HELP);
8701
- return EXIT.pass;
8702
- }
8703
- if (a === "--print")
8704
- print = true;
8705
- else if (a.startsWith("-")) {
8706
- console.error(`firegoat open: unknown flag: ${a}`);
8707
- return EXIT.usage;
8708
- } else
8709
- ids.push(a);
8710
- }
8711
- if (ids.length > 1) {
8712
- console.error("firegoat open: takes at most one run id");
8713
- return EXIT.usage;
8714
- }
8715
- const base = join8(findConfigDir(cwd), ".firegoat", "runs");
8716
- const runs = existsSync6(base) ? readdirSync(base).sort() : [];
8717
- const id = ids[0] ?? runs.at(-1);
8718
- if (!id) {
8719
- console.error(`firegoat open: no runs in ${base}. Run: npx firegoat eval`);
8720
- return EXIT.usage;
8721
- }
8722
- if (!runs.includes(id)) {
8723
- console.error(`firegoat open: no run ${JSON.stringify(id)} in ${base} (latest: ${runs.at(-1) ?? "none"})`);
8724
- return EXIT.usage;
8725
- }
8726
- const dir = join8(base, id);
8727
- console.log(dir);
8728
- const opener = OPENER[process.platform];
8729
- if (print || !opener)
8730
- return EXIT.pass;
8731
- const r = spawnSync5(opener, [dir], { stdio: "inherit" });
8732
- if (r.error || r.status !== 0) {
8733
- console.error(`firegoat open: ${opener} failed (${r.error?.message ?? `exit ${r.status}`}); the run is at the path above`);
8734
- return EXIT.notGraded;
8735
- }
7878
+ ${AGENT_PROMPT}
7879
+ `);
7880
+ if (interactive && copyToClipboard(AGENT_PROMPT))
7881
+ console.log("\x1B[32mPrompt copied!\x1B[0m");
8736
7882
  return EXIT.pass;
8737
7883
  }
8738
7884
 
@@ -8751,24 +7897,8 @@ async function main(argv) {
8751
7897
  return 0;
8752
7898
  case "init":
8753
7899
  return initCommand(rest, process.cwd());
8754
- case "claude":
8755
- if (rest[0] === "login")
8756
- return claudeLoginCommand(rest.slice(1));
8757
- if (rest[0] === "--help" || rest[0] === "-h") {
8758
- console.log(CLAUDE_LOGIN_HELP);
8759
- return 0;
8760
- }
8761
- console.error(`firegoat claude: unknown subcommand
8762
-
8763
- ${CLAUDE_LOGIN_HELP}`);
8764
- return EXIT.usage;
8765
- case "login":
8766
- console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
8767
- return claudeLoginCommand(rest);
8768
7900
  case "eval":
8769
7901
  return evalCommand(rest, process.cwd());
8770
- case "open":
8771
- return openCommand(rest, process.cwd());
8772
7902
  default:
8773
7903
  console.error(`unknown command: ${cmd}
8774
7904