firegoat 0.6.0 → 0.8.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 +220 -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.8). 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, end_goal, check results, paths). Nothing is uploaded.
65
+
66
+ ### Grading: your agent judges the end goal
67
+
68
+ firegoat runs only the script checks; they set the exit code. Each test's `end_goal` is handed over, not graded: whoever drives firegoat (usually your coding agent) judges it 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.8.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/team2027/firegoat-cli.git"
@@ -7208,11 +7208,157 @@ 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, an end_goal, checks
7225
+ 3. ask the human to run once: npx firegoat claude login (opens a browser)
7226
+ 4. npx firegoat eval --json run the tests; JSON summary on stdout
7227
+ 5. judge each run's end_goal yourself, from its run.json, transcript and
7228
+ workdir: firegoat only runs the script checks
7229
+
7230
+ Commands:
7231
+ init write firegoat.yaml; choose how the agent signs in
7232
+ claude login sign claude in to firegoat's own config dir (interactive, once)
7233
+ eval run the tests
7234
+ open open (or --print) a run's folder
7235
+
7236
+ Run firegoat <command> --help for details and examples.
7237
+
7238
+ ${EXIT_CODES}`;
7239
+ }
7240
+ var INIT_HELP = `Usage: firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
7241
+
7242
+ Writes firegoat.yaml in the current folder with starter tests from what the repo
7243
+ ships (package.json, pyproject.toml, go.mod, Cargo.toml, README), and adds
7244
+ .firegoat/ to .gitignore. Edit the tests afterwards; they are templates.
7245
+
7246
+ --yes, -y don't ask questions (default when stdin is not a terminal)
7247
+ --force, -f overwrite an existing firegoat.yaml
7248
+ --skills also run: npx skills add team2027/skills --skill firegoat-onboarding
7249
+ --skill suggesting-eval-prompts (into .claude/skills/)
7250
+ --auth claude the agent signs in with a Claude login: runs \`firegoat claude login\`
7251
+ (interactive: a human must complete it in the browser)
7252
+ --auth api-key the agent signs in with an Anthropic API key, read from stdin when
7253
+ piped (else asked for without echo) and stored in
7254
+ ~/.firegoat/anthropic-api-key (mode 0600), never in the repo
7255
+ Without --auth, init asks (unless --yes). In CI, ANTHROPIC_API_KEY in
7256
+ the env wins.
7257
+
7258
+ firegoat.yaml reference:
7259
+ agents: [claude-code] only claude-code for now
7260
+ runs: 1 runs per test
7261
+ setup: <sh> | [<sh>, ...] once per eval, in the repo, on the host; whatever it
7262
+ writes to $FIREGOAT_OUT is copied into each run and put
7263
+ first on the agent's PATH
7264
+ workdir_setup: <sh> | [...] in each fresh workdir before the agent starts
7265
+ (env: $FIREGOAT_OUT, $FIREGOAT_WORKDIR, vars)
7266
+ vars: {KEY: value} defaults for {{vars.KEY}}; --var KEY=value overrides
7267
+ tests:
7268
+ - name: quickstart letters, digits, '.', '_', '-'; unique
7269
+ prompt: ... what a user would ask the agent; one concrete outcome
7270
+ end_goal: ... what is true when the problem is solved; not graded by
7271
+ firegoat, handed to you (goal: is a deprecated alias)
7272
+ checks: optional shell commands, run in the workdir after the
7273
+ - run: ... agent; exit 0 = pass; they set eval's exit code
7274
+ timeout: 600 optional, seconds
7275
+
7276
+ The agent's folder has no repo files. Checks get $FIREGOAT_WORKDIR,
7277
+ $FIREGOAT_SESSION (stream-json transcript), $FIREGOAT_OUT and the vars.
7278
+ end_goal is not graded by firegoat: it is handed to the caller to judge.
7279
+
7280
+ Example:
7281
+ setup: npm pack --pack-destination "$FIREGOAT_OUT"
7282
+ workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
7283
+ tests:
7284
+ - name: first-call
7285
+ prompt: >-
7286
+ Using the acme SDK (already installed from the build under test), send a test
7287
+ event and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
7288
+ end_goal: >-
7289
+ A script sends one event with the SDK's client (not raw HTTP) and prints its
7290
+ id, without installing acme from the registry.
7291
+ checks:
7292
+ - run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_SESSION"
7293
+ - run: |
7294
+ ! grep -Eq 'npm (install|i) [^"]*acme' "$FIREGOAT_SESSION"
7295
+
7296
+ Next: npx firegoat claude login (a human, once), then npx firegoat eval.
7297
+
7298
+ Exit codes: 0 done, 2 the skills install or claude login failed, 3 usage error.`;
7299
+ var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host]
7300
+ [--claude-bin PATH]
7301
+
7302
+ Runs each test headless: claude gets the prompt in a fresh folder outside the repo,
7303
+ sandboxed on macOS (it can't read your home dir or the repo), with setup:'s outputs
7304
+ on PATH. Then the test's script checks run in that folder.
7305
+
7306
+ test.. only these tests (by name)
7307
+ --runs N runs per test (overrides runs: in firegoat.yaml)
7308
+ --var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
7309
+ --json the summary as JSON on stdout; progress goes to stderr
7310
+ --host no sandbox (required off macOS; use a throwaway container)
7311
+ --claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
7312
+
7313
+ Sign-in: the one chosen in firegoat init (Claude login or stored API key); in CI,
7314
+ ANTHROPIC_API_KEY from the env. eval prints which. Not logged in = exit 2 with the
7315
+ command to run.
7316
+
7317
+ Output: .firegoat/runs/<runId>/
7318
+ summary.json every run, plus totals and a grading hint
7319
+ <test>/<n>/run.json one run: test, n, status and reason (from the checks),
7320
+ prompt, end_goal, checks [{run, code}], and paths:
7321
+ dir, workdir, session, stderr, checksLog
7322
+ <test>/<n>/session.jsonl the agent's transcript, stream-json (one event per line)
7323
+ setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
7324
+
7325
+ Grading: firegoat runs only the script checks; they set the exit code. end_goal is
7326
+ yours to judge, with your own process. session.jsonl is stream-json: the
7327
+ last line has type "result" with the agent's final message; tool calls are
7328
+ "tool_use" items in "assistant" messages; tool output is in "user" messages.
7329
+
7330
+ Examples:
7331
+ npx firegoat eval --json all tests, JSON summary on stdout
7332
+ npx firegoat eval quickstart --runs 3 one test, three runs
7333
+ npx firegoat eval --var SHA=$(git rev-parse HEAD)
7334
+
7335
+ firegoat.yaml is looked up in the current folder, then parents up to the git root.
7336
+ A run can take minutes (default timeout 600s per test).
7337
+
7338
+ ${EXIT_CODES}`;
7339
+ var OPEN_HELP = `Usage: firegoat open [runId] [--print]
7340
+
7341
+ Opens a run's folder (.firegoat/runs/<runId>/) with the system opener; the latest
7342
+ run by default. --print only prints the path (use this from an agent).
7343
+
7344
+ Exit codes: 0 done, 2 the opener failed, 3 no such run.`;
7345
+ var CLAUDE_LOGIN_HELP = `Usage: firegoat claude login [--claude-bin PATH]
7346
+
7347
+ Signs claude in to firegoat's own config dir (~/.firegoat/claude), once per machine.
7348
+ Evals then run on that login, never your ~/.claude. Interactive: it opens a browser,
7349
+ so an agent should ask the human to run it. \`firegoat login\` is a deprecated alias.
7350
+
7351
+ Exit codes: 0 logged in, 2 claude missing or the login failed, 3 usage error.`;
7352
+
7211
7353
  // src/claude-login.ts
7212
7354
  function claudeLoginCommand(args) {
7213
7355
  let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
7214
7356
  for (let i = 0;i < args.length; i++) {
7215
7357
  const a = args[i];
7358
+ if (a === "--help" || a === "-h") {
7359
+ console.log(CLAUDE_LOGIN_HELP);
7360
+ return EXIT.pass;
7361
+ }
7216
7362
  if (a === "--claude-bin" && args[i + 1] !== undefined)
7217
7363
  claudeBin = args[++i];
7218
7364
  else if (a.startsWith("--claude-bin="))
@@ -7363,16 +7509,21 @@ function parseConfig(text, warn = console.error) {
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
- }
7512
+ if ("judge" in c)
7513
+ fail(`${cp}.judge`, "plain-language checks were removed; put the criteria in end_goal (your agent judges it) or write a run: check");
7370
7514
  checks.push({ run: str(c.run, `${cp}.run`) });
7371
7515
  });
7516
+ if (t.rubric !== undefined)
7517
+ fail(`${p}.rubric`, "was removed in firegoat 0.8.0; put the criteria in end_goal");
7518
+ if (t.goal !== undefined && t.end_goal !== undefined)
7519
+ fail(`${p}.goal`, "use end_goal only (goal is its deprecated name)");
7520
+ if (t.goal !== undefined)
7521
+ warn(`${CONFIG_FILE}: ${p}.goal is deprecated; rename it to end_goal`);
7522
+ const endGoalKey = t.goal !== undefined ? "goal" : "end_goal";
7372
7523
  return {
7373
7524
  name,
7374
7525
  prompt: str(t.prompt, `${p}.prompt`),
7375
- goal: str(t.goal, `${p}.goal`),
7526
+ end_goal: str(t[endGoalKey], `${p}.${endGoalKey}`),
7376
7527
  checks,
7377
7528
  timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
7378
7529
  };
@@ -7790,6 +7941,30 @@ function resolveBin(bin) {
7790
7941
 
7791
7942
  // src/eval.ts
7792
7943
  var CHECK_TIMEOUT_MS = 120000;
7944
+ var GRADING_HINT = "firegoat ran only the script checks. Each run's end_goal is for the caller to judge, from its session (stream-json transcript) and workdir.";
7945
+ function blankRun(test, n, dir, prompt) {
7946
+ return {
7947
+ test: test.name,
7948
+ n,
7949
+ status: "error",
7950
+ reason: "",
7951
+ prompt,
7952
+ end_goal: test.end_goal,
7953
+ checks: [],
7954
+ dir,
7955
+ workdir: null,
7956
+ session: join6(dir, "session.jsonl"),
7957
+ stderr: join6(dir, "stderr.txt"),
7958
+ checksLog: join6(dir, "checks.txt"),
7959
+ num_turns: null,
7960
+ cost: null,
7961
+ duration_ms: 0
7962
+ };
7963
+ }
7964
+ function writeRunJson(r) {
7965
+ writeFileSync2(join6(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
7966
+ `);
7967
+ }
7793
7968
 
7794
7969
  class UsageError extends Error {
7795
7970
  }
@@ -7874,23 +8049,13 @@ async function runOne(opts) {
7874
8049
  const started = Date.now();
7875
8050
  const dir = join6(opts.runDir, test.name, String(n));
7876
8051
  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
- };
8052
+ const summary = blankRun(test, n, dir, render(test.prompt, opts.vars));
8053
+ const session = summary.session;
7890
8054
  const done = (status, reason) => {
7891
8055
  summary.status = status;
7892
8056
  summary.reason = reason;
7893
8057
  summary.duration_ms = Date.now() - started;
8058
+ writeRunJson(summary);
7894
8059
  return summary;
7895
8060
  };
7896
8061
  const { runRoot, workdir } = newRunRoot(`${test.name}-${n}`);
@@ -7923,10 +8088,10 @@ async function runOne(opts) {
7923
8088
  keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
7924
8089
  profile: sandbox.profile,
7925
8090
  bin: opts.claudeBin,
7926
- prompt: render(test.prompt, opts.vars),
8091
+ prompt: summary.prompt,
7927
8092
  cwd: workdir,
7928
8093
  sessionPath: session,
7929
- stderrPath: join6(dir, "stderr.txt"),
8094
+ stderrPath: summary.stderr,
7930
8095
  timeoutMs: test.timeout * 1000
7931
8096
  });
7932
8097
  if (outcome.result) {
@@ -7935,11 +8100,9 @@ async function runOne(opts) {
7935
8100
  }
7936
8101
  if (!outcome.ok)
7937
8102
  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"));
8103
+ if (test.checks.length === 0)
8104
+ return done("pass", "no script checks; end_goal is for the caller to judge");
8105
+ const log = createWriteStream3(summary.checksLog);
7943
8106
  const env = {
7944
8107
  ...process.env,
7945
8108
  ...hostEnv,
@@ -7961,7 +8124,7 @@ async function runOne(opts) {
7961
8124
  } finally {
7962
8125
  await new Promise((r) => log.end(r));
7963
8126
  }
7964
- summary.checks = results.map((r) => ({ run: r.run, code: r.code }));
8127
+ summary.checks = results.map((r) => r.error === undefined ? { run: r.run, code: r.code } : { run: r.run, code: r.code, error: r.error });
7965
8128
  const broken = results.find((r) => r.notStarted);
7966
8129
  if (broken)
7967
8130
  return done("error", `not graded: check ${broken.run}: ${broken.error}`);
@@ -7971,6 +8134,10 @@ async function runOne(opts) {
7971
8134
  return done("pass", `${results.length}/${results.length} checks passed`);
7972
8135
  }
7973
8136
  async function evalCommand(args, cwd) {
8137
+ if (args.includes("--help") || args.includes("-h")) {
8138
+ console.log(EVAL_HELP);
8139
+ return EXIT.pass;
8140
+ }
7974
8141
  let opts;
7975
8142
  try {
7976
8143
  opts = parseArgs(args);
@@ -8079,6 +8246,7 @@ async function evalCommand(args, cwd) {
8079
8246
  runsDir: runDir,
8080
8247
  isolation: opts.host ? "host" : "seatbelt",
8081
8248
  auth: auth.method,
8249
+ grading: GRADING_HINT,
8082
8250
  totals,
8083
8251
  runs: results
8084
8252
  };
@@ -8103,18 +8271,11 @@ async function evalCommand(args, cwd) {
8103
8271
  warn
8104
8272
  });
8105
8273
  } 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
- };
8274
+ const dir = join6(runDir, test.name, String(n));
8275
+ mkdirSync5(dir, { recursive: true });
8276
+ r = blankRun(test, n, dir, test.prompt);
8277
+ r.reason = `not graded: firegoat crashed: ${e.message}`;
8278
+ writeRunJson(r);
8118
8279
  }
8119
8280
  results.push(r);
8120
8281
  writeSummary();
@@ -8126,6 +8287,7 @@ async function evalCommand(args, cwd) {
8126
8287
  say(`
8127
8288
  ${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
8128
8289
  say(`runs: ${runDir}`);
8290
+ say(`end_goal is not graded by firegoat; per run: ${join6(runDir, "<test>", "<n>", "run.json")}`);
8129
8291
  if (opts.json)
8130
8292
  console.log(JSON.stringify(doc, null, 2));
8131
8293
  return error > 0 ? EXIT.notGraded : fail > 0 ? EXIT.fail : EXIT.pass;
@@ -8137,17 +8299,6 @@ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync
8137
8299
  import { basename as basename2, join as join7 } from "node:path";
8138
8300
  import { createInterface as createInterface2 } from "node:readline/promises";
8139
8301
  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
8302
  var SKILLS = ["firegoat-onboarding", "suggesting-eval-prompts"];
8152
8303
  var SKILLS_ARGV = [
8153
8304
  "skills",
@@ -8266,7 +8417,7 @@ function templates(p) {
8266
8417
  out.push({
8267
8418
  name: "cli-first-run",
8268
8419
  prompt: `I just heard about the \`${p.bin}\` command-line tool from ${p.name} and want to see it actually do something useful. Use it for one basic real task and save what it prints to cli-output.txt. Keep it minimal. Use only ${docs} as a guide.`,
8269
- goal: `cli-output.txt exists and holds real output from running \`${p.bin}\`, not an install log or an error.`,
8420
+ end_goal: `cli-output.txt exists and holds real output from running \`${p.bin}\`, not an install log or an error.`,
8270
8421
  checks: [
8271
8422
  "test -s cli-output.txt",
8272
8423
  "! grep -qiE 'command not found|not recognized|ENOENT' cli-output.txt"
@@ -8277,7 +8428,7 @@ function templates(p) {
8277
8428
  out.push({
8278
8429
  name: "quickstart",
8279
8430
  prompt: `I want to use ${p.name} in a new ${LANG[p.ecosystem]} project. Get the smallest working example going: a program that uses ${p.name} to do one real thing, then run it and save what it prints to output.txt. Keep it minimal. Use only ${docs} as a guide.`,
8280
- goal: `A program in the workdir uses ${p.name}, runs without errors, and its output is saved in output.txt.`,
8431
+ end_goal: `A program in the workdir uses ${p.name}, runs without errors, and its output is saved in output.txt.`,
8281
8432
  checks: [
8282
8433
  "test -s output.txt",
8283
8434
  `grep -rqsF ${GREP_EXCLUDES} -- ${sq(importName(p))} .`
@@ -8288,7 +8439,7 @@ function templates(p) {
8288
8439
  out.push({
8289
8440
  name: "quickstart",
8290
8441
  prompt: `I want to try ${p.name}. Follow its quickstart in a new project, get the smallest example working, then run it and save what it prints to output.txt. Keep it minimal. Use only ${docs} as a guide.`,
8291
- goal: `The ${p.name} quickstart example runs without errors and its output is saved in output.txt.`,
8442
+ end_goal: `The ${p.name} quickstart example runs without errors and its output is saved in output.txt.`,
8292
8443
  checks: ["test -s output.txt"]
8293
8444
  });
8294
8445
  }
@@ -8337,7 +8488,7 @@ function renderYaml(p, tests) {
8337
8488
  "tests:"
8338
8489
  ];
8339
8490
  for (const t of tests) {
8340
- 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");
8491
+ lines.push(` - name: ${t.name} # letters, digits, '.', '_' and '-'`, " # What a user would ask the agent. Edit this.", " prompt: >-", block(t.prompt, " "), " # One sentence that is true when the user's problem is solved. Not graded by", " # firegoat: it goes to your agent (eval --json, run.json) to judge.", " end_goal: >-", block(t.end_goal, " "), " # Shell commands run in the agent's workdir afterwards; exit 0 = pass.", " checks:", ...t.checks.map((c) => ` - run: ${JSON.stringify(c)}`), " # Seconds before the agent run is stopped.", " timeout: 600");
8341
8492
  }
8342
8493
  return `${lines.join(`
8343
8494
  `)}
@@ -8484,12 +8635,12 @@ async function initCommand(args, cwd) {
8484
8635
  break;
8485
8636
  case "--help":
8486
8637
  case "-h":
8487
- console.log(INIT_USAGE);
8638
+ console.log(INIT_HELP);
8488
8639
  return 0;
8489
8640
  default:
8490
8641
  console.error(`firegoat init: unknown argument: ${a}
8491
8642
 
8492
- ${INIT_USAGE}`);
8643
+ ${INIT_HELP}`);
8493
8644
  return EXIT.usage;
8494
8645
  }
8495
8646
  }
@@ -8545,6 +8696,10 @@ function openCommand(args, cwd) {
8545
8696
  let print = false;
8546
8697
  const ids = [];
8547
8698
  for (const a of args) {
8699
+ if (a === "--help" || a === "-h") {
8700
+ console.log(OPEN_HELP);
8701
+ return EXIT.pass;
8702
+ }
8548
8703
  if (a === "--print")
8549
8704
  print = true;
8550
8705
  else if (a.startsWith("-")) {
@@ -8582,33 +8737,6 @@ function openCommand(args, cwd) {
8582
8737
  }
8583
8738
 
8584
8739
  // 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
8740
  async function main(argv) {
8613
8741
  const [cmd, ...rest] = argv;
8614
8742
  switch (cmd) {
@@ -8619,14 +8747,20 @@ async function main(argv) {
8619
8747
  case undefined:
8620
8748
  case "--help":
8621
8749
  case "-h":
8622
- console.log(USAGE);
8750
+ console.log(topHelp(package_default.version));
8623
8751
  return 0;
8624
8752
  case "init":
8625
8753
  return initCommand(rest, process.cwd());
8626
8754
  case "claude":
8627
8755
  if (rest[0] === "login")
8628
8756
  return claudeLoginCommand(rest.slice(1));
8629
- console.error(`usage: firegoat claude login [--claude-bin PATH]`);
8757
+ if (rest[0] === "--help" || rest[0] === "-h") {
8758
+ console.log(CLAUDE_LOGIN_HELP);
8759
+ return 0;
8760
+ }
8761
+ console.error(`firegoat claude: unknown subcommand
8762
+
8763
+ ${CLAUDE_LOGIN_HELP}`);
8630
8764
  return EXIT.usage;
8631
8765
  case "login":
8632
8766
  console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
@@ -8638,7 +8772,7 @@ async function main(argv) {
8638
8772
  default:
8639
8773
  console.error(`unknown command: ${cmd}
8640
8774
 
8641
- ${USAGE}`);
8775
+ ${topHelp(package_default.version)}`);
8642
8776
  return EXIT.usage;
8643
8777
  }
8644
8778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/team2027/firegoat-cli.git"