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.
- package/README.md +12 -4
- package/dist/cli.js +225 -86
- 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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
7368
|
-
|
|
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
|
|
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")
|
|
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
|
|
7878
|
-
const
|
|
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:
|
|
8096
|
+
prompt: summary.prompt,
|
|
7927
8097
|
cwd: workdir,
|
|
7928
8098
|
sessionPath: session,
|
|
7929
|
-
stderrPath:
|
|
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
|
-
|
|
7940
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
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(
|
|
8643
|
+
console.log(INIT_HELP);
|
|
8488
8644
|
return 0;
|
|
8489
8645
|
default:
|
|
8490
8646
|
console.error(`firegoat init: unknown argument: ${a}
|
|
8491
8647
|
|
|
8492
|
-
${
|
|
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(
|
|
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
|
-
|
|
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
|
-
${
|
|
8780
|
+
${topHelp(package_default.version)}`);
|
|
8642
8781
|
return EXIT.usage;
|
|
8643
8782
|
}
|
|
8644
8783
|
}
|