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