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.
- package/README.md +12 -4
- package/dist/cli.js +220 -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.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
|
|
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
|
|
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.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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
};
|
|
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:
|
|
8091
|
+
prompt: summary.prompt,
|
|
7927
8092
|
cwd: workdir,
|
|
7928
8093
|
sessionPath: session,
|
|
7929
|
-
stderrPath:
|
|
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
|
-
|
|
7940
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
};
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.", "
|
|
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(
|
|
8638
|
+
console.log(INIT_HELP);
|
|
8488
8639
|
return 0;
|
|
8489
8640
|
default:
|
|
8490
8641
|
console.error(`firegoat init: unknown argument: ${a}
|
|
8491
8642
|
|
|
8492
|
-
${
|
|
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(
|
|
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
|
-
|
|
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
|
-
${
|
|
8775
|
+
${topHelp(package_default.version)}`);
|
|
8642
8776
|
return EXIT.usage;
|
|
8643
8777
|
}
|
|
8644
8778
|
}
|