firegoat 0.6.0 → 0.7.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 +12 -4
  2. package/dist/cli.js +225 -86
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Local-first AX evals: how well coding agents use your SDK, CLI, API and docs.
4
4
 
5
- Status: early (v0.6). See `plan.md`.
5
+ Status: early (v0.7). See `plan.md`.
6
6
 
7
7
  ## Use
8
8
 
@@ -37,6 +37,8 @@ Then say so in the prompt: "The package is already installed from the build unde
37
37
  - Checks also get `$FIREGOAT_OUT` (the run's copy).
38
38
  - A failed `setup:` or `workdir_setup:` means not graded (exit 2); logs are `.firegoat/runs/<runId>/setup.txt` and `<test>/<n>/workdir_setup.txt`.
39
39
 
40
+ Want the agent to run a plain `npm i acme-sdk` (or `pip install`) and still get your build, with no hint in the prompt? See the recipe in [`examples/local-npm-registry`](examples/local-npm-registry/README.md): a throwaway local registry started from `setup:`.
41
+
40
42
  ### How the agent signs in
41
43
 
42
44
  `firegoat init` asks (or pass `--auth`):
@@ -59,14 +61,20 @@ A probe checks the sandbox before every run; if it does not hold, the run is not
59
61
 
60
62
  Off macOS there is no sandbox yet: run inside a throwaway container and pass `--host`.
61
63
 
62
- `eval` saves each run's transcript to `.firegoat/runs/<runId>/<test>/<n>/session.jsonl` and a `summary.json`, then runs the test's `checks` in the agent's folder. Nothing is uploaded. `firegoat.yaml` is looked up in the current directory, then parents up to the git root.
64
+ `eval` saves each run's transcript to `.firegoat/runs/<runId>/<test>/<n>/session.jsonl`, runs the test's `checks` in the agent's folder, and writes `run.json` per run plus `summary.json` (prompt, goal, rubric, check results, paths). Nothing is uploaded.
65
+
66
+ ### Grading: your agent judges the goal
67
+
68
+ firegoat runs only the script checks; they set the exit code. The `goal` and an optional `rubric:` list are handed over, not graded: whoever drives firegoat (usually your coding agent) judges them with its own process from `npx firegoat eval --json` / `run.json`, the transcript and the workdir. firegoat records no verdict beyond the checks.
69
+
70
+ Every command has `--help` written for an agent; start with `npx firegoat --help`. `firegoat.yaml` is looked up in the current directory, then parents up to the git root.
63
71
 
64
72
  Exit codes:
65
73
 
66
74
  | code | meaning |
67
75
  |---|---|
68
- | 0 | every run passed |
69
- | 1 | a goal or check failed |
76
+ | 0 | every run's script checks passed |
77
+ | 1 | a script check failed |
70
78
  | 2 | at least one run was not graded (claude missing, auth, crash, timeout); wins over 1 |
71
79
  | 3 | usage or config error (bad flag, unknown test, invalid or missing `firegoat.yaml`) |
72
80
 
package/dist/cli.js CHANGED
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
6947
6947
  // package.json
6948
6948
  var package_default = {
6949
6949
  name: "firegoat",
6950
- version: "0.6.0",
6950
+ version: "0.7.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/team2027/firegoat-cli.git"
@@ -7208,11 +7208,156 @@ function resolveAuth(env) {
7208
7208
  // src/exit.ts
7209
7209
  var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
7210
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
+
7211
7352
  // src/claude-login.ts
7212
7353
  function claudeLoginCommand(args) {
7213
7354
  let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
7214
7355
  for (let i = 0;i < args.length; i++) {
7215
7356
  const a = args[i];
7357
+ if (a === "--help" || a === "-h") {
7358
+ console.log(CLAUDE_LOGIN_HELP);
7359
+ return EXIT.pass;
7360
+ }
7216
7361
  if (a === "--claude-bin" && args[i + 1] !== undefined)
7217
7362
  claudeBin = args[++i];
7218
7363
  else if (a.startsWith("--claude-bin="))
@@ -7319,7 +7464,7 @@ function posInt(v, path, fallback) {
7319
7464
  fail(path, "must be a positive integer");
7320
7465
  return v;
7321
7466
  }
7322
- function parseConfig(text, warn = console.error) {
7467
+ function parseConfig(text) {
7323
7468
  let raw;
7324
7469
  try {
7325
7470
  raw = $parse(text);
@@ -7359,20 +7504,21 @@ function parseConfig(text, warn = console.error) {
7359
7504
  if (!Array.isArray(checksRaw))
7360
7505
  fail(`${p}.checks`, "must be a list");
7361
7506
  const checks = [];
7507
+ const rubric = rubricOf(t.rubric, `${p}.rubric`);
7362
7508
  checksRaw.forEach((c, j) => {
7363
7509
  const cp = `${p}.checks[${j}]`;
7364
7510
  if (!isRecord(c))
7365
7511
  fail(cp, "must be a mapping");
7366
- if ("judge" in c) {
7367
- warn(`${CONFIG_FILE}: ${cp}: judge checks need upload, which v1 does not do; skipped`);
7368
- return;
7369
- }
7370
- checks.push({ run: str(c.run, `${cp}.run`) });
7512
+ if ("judge" in c)
7513
+ rubric.push(str(c.judge, `${cp}.judge`));
7514
+ else
7515
+ checks.push({ run: str(c.run, `${cp}.run`) });
7371
7516
  });
7372
7517
  return {
7373
7518
  name,
7374
7519
  prompt: str(t.prompt, `${p}.prompt`),
7375
7520
  goal: str(t.goal, `${p}.goal`),
7521
+ rubric,
7376
7522
  checks,
7377
7523
  timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
7378
7524
  };
@@ -7387,6 +7533,15 @@ function parseConfig(text, warn = console.error) {
7387
7533
  };
7388
7534
  }
7389
7535
  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
+ }
7390
7545
  function commands(raw, path) {
7391
7546
  if (raw === undefined)
7392
7547
  return [];
@@ -7424,11 +7579,11 @@ function render(text, vars) {
7424
7579
  return v;
7425
7580
  });
7426
7581
  }
7427
- function loadConfig(dir, warn) {
7582
+ function loadConfig(dir) {
7428
7583
  const file = `${dir}/${CONFIG_FILE}`;
7429
7584
  if (!existsSync3(file))
7430
7585
  throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
7431
- return parseConfig(readFileSync2(file, "utf8"), warn);
7586
+ return parseConfig(readFileSync2(file, "utf8"));
7432
7587
  }
7433
7588
  function findConfigDir(cwd) {
7434
7589
  let dir = cwd;
@@ -7790,6 +7945,31 @@ function resolveBin(bin) {
7790
7945
 
7791
7946
  // src/eval.ts
7792
7947
  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
+ }
7793
7973
 
7794
7974
  class UsageError extends Error {
7795
7975
  }
@@ -7874,23 +8054,13 @@ async function runOne(opts) {
7874
8054
  const started = Date.now();
7875
8055
  const dir = join6(opts.runDir, test.name, String(n));
7876
8056
  mkdirSync5(dir, { recursive: true });
7877
- const session = join6(dir, "session.jsonl");
7878
- const summary = {
7879
- test: test.name,
7880
- n,
7881
- status: "error",
7882
- reason: "",
7883
- workdir: null,
7884
- session,
7885
- checks: [],
7886
- num_turns: null,
7887
- cost: null,
7888
- duration_ms: 0
7889
- };
8057
+ const summary = blankRun(test, n, dir, render(test.prompt, opts.vars));
8058
+ const session = summary.session;
7890
8059
  const done = (status, reason) => {
7891
8060
  summary.status = status;
7892
8061
  summary.reason = reason;
7893
8062
  summary.duration_ms = Date.now() - started;
8063
+ writeRunJson(summary);
7894
8064
  return summary;
7895
8065
  };
7896
8066
  const { runRoot, workdir } = newRunRoot(`${test.name}-${n}`);
@@ -7923,10 +8093,10 @@ async function runOne(opts) {
7923
8093
  keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
7924
8094
  profile: sandbox.profile,
7925
8095
  bin: opts.claudeBin,
7926
- prompt: render(test.prompt, opts.vars),
8096
+ prompt: summary.prompt,
7927
8097
  cwd: workdir,
7928
8098
  sessionPath: session,
7929
- stderrPath: join6(dir, "stderr.txt"),
8099
+ stderrPath: summary.stderr,
7930
8100
  timeoutMs: test.timeout * 1000
7931
8101
  });
7932
8102
  if (outcome.result) {
@@ -7935,11 +8105,9 @@ async function runOne(opts) {
7935
8105
  }
7936
8106
  if (!outcome.ok)
7937
8107
  return done("error", `not graded: ${outcome.reason}`);
7938
- if (test.checks.length === 0) {
7939
- opts.warn(`warning: ${test.name} has no checks, so its goal is unverified; counted as pass`);
7940
- return done("pass", "no checks (goal unverified)");
7941
- }
7942
- const log = createWriteStream3(join6(dir, "checks.txt"));
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);
7943
8111
  const env = {
7944
8112
  ...process.env,
7945
8113
  ...hostEnv,
@@ -7961,7 +8129,7 @@ async function runOne(opts) {
7961
8129
  } finally {
7962
8130
  await new Promise((r) => log.end(r));
7963
8131
  }
7964
- summary.checks = results.map((r) => ({ run: r.run, code: r.code }));
8132
+ summary.checks = results.map((r) => r.error === undefined ? { run: r.run, code: r.code } : { run: r.run, code: r.code, error: r.error });
7965
8133
  const broken = results.find((r) => r.notStarted);
7966
8134
  if (broken)
7967
8135
  return done("error", `not graded: check ${broken.run}: ${broken.error}`);
@@ -7971,6 +8139,10 @@ async function runOne(opts) {
7971
8139
  return done("pass", `${results.length}/${results.length} checks passed`);
7972
8140
  }
7973
8141
  async function evalCommand(args, cwd) {
8142
+ if (args.includes("--help") || args.includes("-h")) {
8143
+ console.log(EVAL_HELP);
8144
+ return EXIT.pass;
8145
+ }
7974
8146
  let opts;
7975
8147
  try {
7976
8148
  opts = parseArgs(args);
@@ -7984,7 +8156,7 @@ async function evalCommand(args, cwd) {
7984
8156
  const say = opts.json ? console.error : console.log;
7985
8157
  const warn = console.error;
7986
8158
  const root = findConfigDir(cwd);
7987
- const config = loadConfig(root, warn);
8159
+ const config = loadConfig(root);
7988
8160
  let tests = config.tests;
7989
8161
  if (opts.names.length > 0) {
7990
8162
  const known = new Set(tests.map((t) => t.name));
@@ -8079,6 +8251,7 @@ async function evalCommand(args, cwd) {
8079
8251
  runsDir: runDir,
8080
8252
  isolation: opts.host ? "host" : "seatbelt",
8081
8253
  auth: auth.method,
8254
+ grading: GRADING_HINT,
8082
8255
  totals,
8083
8256
  runs: results
8084
8257
  };
@@ -8103,18 +8276,11 @@ async function evalCommand(args, cwd) {
8103
8276
  warn
8104
8277
  });
8105
8278
  } catch (e) {
8106
- r = {
8107
- test: test.name,
8108
- n,
8109
- status: "error",
8110
- reason: `not graded: firegoat crashed: ${e.message}`,
8111
- workdir: null,
8112
- session: join6(runDir, test.name, String(n), "session.jsonl"),
8113
- checks: [],
8114
- num_turns: null,
8115
- cost: null,
8116
- duration_ms: 0
8117
- };
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);
8118
8284
  }
8119
8285
  results.push(r);
8120
8286
  writeSummary();
@@ -8126,6 +8292,7 @@ async function evalCommand(args, cwd) {
8126
8292
  say(`
8127
8293
  ${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
8128
8294
  say(`runs: ${runDir}`);
8295
+ say(`goal and rubric are not graded by firegoat; per run: ${join6(runDir, "<test>", "<n>", "run.json")}`);
8129
8296
  if (opts.json)
8130
8297
  console.log(JSON.stringify(doc, null, 2));
8131
8298
  return error > 0 ? EXIT.notGraded : fail > 0 ? EXIT.fail : EXIT.pass;
@@ -8137,17 +8304,6 @@ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync
8137
8304
  import { basename as basename2, join as join7 } from "node:path";
8138
8305
  import { createInterface as createInterface2 } from "node:readline/promises";
8139
8306
  import { Writable } from "node:stream";
8140
- var INIT_USAGE = `Usage: firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
8141
-
8142
- --yes, -y don't ask questions (default when stdin is not a terminal)
8143
- --force, -f overwrite an existing ${CONFIG_FILE}
8144
- --skills install the firegoat-onboarding and suggesting-eval-prompts skills
8145
- into .claude/skills/ (npx skills add team2027/skills)
8146
- --auth claude how the agent signs in: run \`firegoat claude login\` (your Claude plan)
8147
- --auth api-key store an Anthropic API key for firegoat only (~/.firegoat, mode 0600),
8148
- read from stdin when piped, else asked for without echo
8149
- Without --auth, init asks (unless --yes). In CI, ANTHROPIC_API_KEY in env wins.
8150
- `;
8151
8307
  var SKILLS = ["firegoat-onboarding", "suggesting-eval-prompts"];
8152
8308
  var SKILLS_ARGV = [
8153
8309
  "skills",
@@ -8484,12 +8640,12 @@ async function initCommand(args, cwd) {
8484
8640
  break;
8485
8641
  case "--help":
8486
8642
  case "-h":
8487
- console.log(INIT_USAGE);
8643
+ console.log(INIT_HELP);
8488
8644
  return 0;
8489
8645
  default:
8490
8646
  console.error(`firegoat init: unknown argument: ${a}
8491
8647
 
8492
- ${INIT_USAGE}`);
8648
+ ${INIT_HELP}`);
8493
8649
  return EXIT.usage;
8494
8650
  }
8495
8651
  }
@@ -8545,6 +8701,10 @@ function openCommand(args, cwd) {
8545
8701
  let print = false;
8546
8702
  const ids = [];
8547
8703
  for (const a of args) {
8704
+ if (a === "--help" || a === "-h") {
8705
+ console.log(OPEN_HELP);
8706
+ return EXIT.pass;
8707
+ }
8548
8708
  if (a === "--print")
8549
8709
  print = true;
8550
8710
  else if (a.startsWith("-")) {
@@ -8582,33 +8742,6 @@ function openCommand(args, cwd) {
8582
8742
  }
8583
8743
 
8584
8744
  // src/cli.ts
8585
- var USAGE = `firegoat ${package_default.version}: local-first AX evals
8586
-
8587
- Usage:
8588
- firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
8589
- write firegoat.yaml, choose how the agent signs in
8590
- firegoat claude login [--claude-bin PATH] sign claude in to firegoat's own config dir (once)
8591
- firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host] [--claude-bin PATH]
8592
- run evals with claude in a sandboxed fresh workdir
8593
- firegoat open [runId] [--print] open a run's folder (default: the latest)
8594
- firegoat --version
8595
-
8596
- eval options:
8597
- test.. only run these tests (by name)
8598
- --runs N runs per test (overrides runs: in firegoat.yaml)
8599
- --var KEY=value fills {{vars.KEY}} in prompts and checks; env for setup, workdir_setup and checks
8600
- --json JSON summary on stdout, progress on stderr
8601
- --host no sandbox (needed off macOS; use a throwaway container)
8602
- --claude-bin PATH claude binary to run (env: FIREGOAT_CLAUDE_BIN; default: claude on PATH)
8603
-
8604
- firegoat.yaml is looked up in the current dir, then parents up to the git root.
8605
-
8606
- Exit codes:
8607
- 0 every run passed
8608
- 1 a goal or check failed
8609
- 2 at least one run was not graded (claude missing, auth, crash, timeout); wins over 1
8610
- 3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)
8611
- `;
8612
8745
  async function main(argv) {
8613
8746
  const [cmd, ...rest] = argv;
8614
8747
  switch (cmd) {
@@ -8619,14 +8752,20 @@ async function main(argv) {
8619
8752
  case undefined:
8620
8753
  case "--help":
8621
8754
  case "-h":
8622
- console.log(USAGE);
8755
+ console.log(topHelp(package_default.version));
8623
8756
  return 0;
8624
8757
  case "init":
8625
8758
  return initCommand(rest, process.cwd());
8626
8759
  case "claude":
8627
8760
  if (rest[0] === "login")
8628
8761
  return claudeLoginCommand(rest.slice(1));
8629
- console.error(`usage: firegoat claude login [--claude-bin PATH]`);
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}`);
8630
8769
  return EXIT.usage;
8631
8770
  case "login":
8632
8771
  console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
@@ -8638,7 +8777,7 @@ async function main(argv) {
8638
8777
  default:
8639
8778
  console.error(`unknown command: ${cmd}
8640
8779
 
8641
- ${USAGE}`);
8780
+ ${topHelp(package_default.version)}`);
8642
8781
  return EXIT.usage;
8643
8782
  }
8644
8783
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/team2027/firegoat-cli.git"