firegoat 0.8.0 → 0.9.1
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 +72 -50
- package/dist/cli.js +474 -1344
- package/package.json +4 -6
package/dist/cli.js
CHANGED
|
@@ -6947,19 +6947,17 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.9.1",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
|
-
url: "git+https://github.com/
|
|
6953
|
+
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
6954
6954
|
},
|
|
6955
6955
|
devDependencies: {
|
|
6956
6956
|
"@biomejs/biome": "^2.5.15",
|
|
6957
6957
|
"@types/bun": "latest",
|
|
6958
|
+
typescript: "^7",
|
|
6958
6959
|
yaml: "^2.9.1"
|
|
6959
6960
|
},
|
|
6960
|
-
peerDependencies: {
|
|
6961
|
-
typescript: "^7"
|
|
6962
|
-
},
|
|
6963
6961
|
bin: {
|
|
6964
6962
|
firegoat: "dist/cli.js"
|
|
6965
6963
|
},
|
|
@@ -6970,7 +6968,7 @@ var package_default = {
|
|
|
6970
6968
|
files: [
|
|
6971
6969
|
"dist"
|
|
6972
6970
|
],
|
|
6973
|
-
homepage: "https://github.com/
|
|
6971
|
+
homepage: "https://github.com/firegoat-ai/firegoat-cli#readme",
|
|
6974
6972
|
license: "MIT",
|
|
6975
6973
|
publishConfig: {
|
|
6976
6974
|
access: "public"
|
|
@@ -6987,412 +6985,8 @@ var package_default = {
|
|
|
6987
6985
|
packageManager: "bun@1.4.3"
|
|
6988
6986
|
};
|
|
6989
6987
|
|
|
6990
|
-
// src/claude-login.ts
|
|
6991
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6992
|
-
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
6993
|
-
|
|
6994
|
-
// src/auth.ts
|
|
6995
|
-
import {
|
|
6996
|
-
chmodSync,
|
|
6997
|
-
existsSync as existsSync2,
|
|
6998
|
-
mkdirSync as mkdirSync2,
|
|
6999
|
-
readFileSync,
|
|
7000
|
-
writeFileSync
|
|
7001
|
-
} from "node:fs";
|
|
7002
|
-
import { join as join2 } from "node:path";
|
|
7003
|
-
|
|
7004
|
-
// src/sandbox.ts
|
|
7005
|
-
import { spawnSync } from "node:child_process";
|
|
7006
|
-
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
7007
|
-
import { homedir } from "node:os";
|
|
7008
|
-
import { basename, dirname, join } from "node:path";
|
|
7009
|
-
var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
7010
|
-
function firegoatHome() {
|
|
7011
|
-
return process.env.FIREGOAT_HOME || join(homedir(), ".firegoat");
|
|
7012
|
-
}
|
|
7013
|
-
function claudeConfigDir() {
|
|
7014
|
-
return join(firegoatHome(), "claude");
|
|
7015
|
-
}
|
|
7016
|
-
var TOOLCHAINS = [
|
|
7017
|
-
".nvm",
|
|
7018
|
-
".fnm",
|
|
7019
|
-
".volta",
|
|
7020
|
-
".bun",
|
|
7021
|
-
".deno",
|
|
7022
|
-
".cargo",
|
|
7023
|
-
".rustup",
|
|
7024
|
-
".pyenv",
|
|
7025
|
-
".rbenv",
|
|
7026
|
-
".asdf",
|
|
7027
|
-
".sdkman",
|
|
7028
|
-
".local/share/mise",
|
|
7029
|
-
".local/share/fnm",
|
|
7030
|
-
".local/share/claude",
|
|
7031
|
-
".local/share/uv",
|
|
7032
|
-
"go",
|
|
7033
|
-
"Library/pnpm"
|
|
7034
|
-
];
|
|
7035
|
-
var TOOLCHAIN_SECRETS = [".cargo/credentials", ".cargo/credentials.toml"];
|
|
7036
|
-
function real(p) {
|
|
7037
|
-
return realpathSync(p);
|
|
7038
|
-
}
|
|
7039
|
-
function under(p, dir) {
|
|
7040
|
-
return p === dir || p.startsWith(`${dir}/`);
|
|
7041
|
-
}
|
|
7042
|
-
function readableToolDirs(home, claudeBin) {
|
|
7043
|
-
const dirs = new Set;
|
|
7044
|
-
const add = (p) => {
|
|
7045
|
-
if (existsSync(p))
|
|
7046
|
-
dirs.add(real(p));
|
|
7047
|
-
};
|
|
7048
|
-
for (const t of TOOLCHAINS)
|
|
7049
|
-
add(join(home, t));
|
|
7050
|
-
for (const p of (process.env.PATH ?? "").split(":")) {
|
|
7051
|
-
if (!p.startsWith("/") || !existsSync(p))
|
|
7052
|
-
continue;
|
|
7053
|
-
const r = real(p);
|
|
7054
|
-
if (!under(r, home) || r === home)
|
|
7055
|
-
continue;
|
|
7056
|
-
const prefix = dirname(r);
|
|
7057
|
-
dirs.add(basename(r) === "bin" && prefix !== home && prefix !== join(home, ".local") ? prefix : r);
|
|
7058
|
-
}
|
|
7059
|
-
add(dirname(dirname(process.execPath)));
|
|
7060
|
-
add(dirname(claudeBin));
|
|
7061
|
-
add(dirname(real(claudeBin)));
|
|
7062
|
-
return [...dirs];
|
|
7063
|
-
}
|
|
7064
|
-
var q = (s) => JSON.stringify(s);
|
|
7065
|
-
function sandboxProfile(opts) {
|
|
7066
|
-
const home = real(homedir());
|
|
7067
|
-
const reads = readableToolDirs(home, opts.claudeBin).map((d) => `(subpath ${q(d)})`);
|
|
7068
|
-
return `(version 1)
|
|
7069
|
-
(allow default)
|
|
7070
|
-
|
|
7071
|
-
(deny file-read* (subpath ${q(home)}))
|
|
7072
|
-
(allow file-read-metadata)
|
|
7073
|
-
(allow file-read*
|
|
7074
|
-
${reads.join(`
|
|
7075
|
-
`)}
|
|
7076
|
-
(subpath ${q(join(home, "Library/Keychains"))})
|
|
7077
|
-
(regex ${q(`^${home}/Library/Preferences/com\\.apple\\.security`)})
|
|
7078
|
-
(regex ${q(`^${home}/Library/Preferences/\\.GlobalPreferences`)}))
|
|
7079
|
-
(deny file-read* file-write*
|
|
7080
|
-
${TOOLCHAIN_SECRETS.map((s) => `(literal ${q(join(home, s))})`).join(`
|
|
7081
|
-
`)})
|
|
7082
|
-
|
|
7083
|
-
(deny file-write* (subpath "/"))
|
|
7084
|
-
(allow file-write*
|
|
7085
|
-
(subpath "/dev")
|
|
7086
|
-
(subpath "/private/tmp")
|
|
7087
|
-
(subpath "/private/var/folders"))
|
|
7088
|
-
(allow file-read* file-write*
|
|
7089
|
-
(subpath ${q(real(opts.configDir))})
|
|
7090
|
-
(subpath ${q(real(opts.runRoot))}))
|
|
7091
|
-
|
|
7092
|
-
(deny file-read* file-write* (subpath ${q(real(opts.repo))}))
|
|
7093
|
-
`;
|
|
7094
|
-
}
|
|
7095
|
-
function agentEnv(runRoot, apiKey) {
|
|
7096
|
-
const env = { ...process.env };
|
|
7097
|
-
delete env.ANTHROPIC_API_KEY;
|
|
7098
|
-
if (apiKey)
|
|
7099
|
-
env.ANTHROPIC_API_KEY = apiKey;
|
|
7100
|
-
const cache = join(runRoot, "cache");
|
|
7101
|
-
const tmp = join(runRoot, "tmp");
|
|
7102
|
-
mkdirSync(cache, { recursive: true });
|
|
7103
|
-
mkdirSync(tmp, { recursive: true });
|
|
7104
|
-
return {
|
|
7105
|
-
...env,
|
|
7106
|
-
CLAUDE_CONFIG_DIR: claudeConfigDir(),
|
|
7107
|
-
TMPDIR: tmp,
|
|
7108
|
-
XDG_CACHE_HOME: cache,
|
|
7109
|
-
npm_config_cache: join(cache, "npm"),
|
|
7110
|
-
npm_config_store_dir: join(cache, "pnpm-store"),
|
|
7111
|
-
YARN_CACHE_FOLDER: join(cache, "yarn"),
|
|
7112
|
-
BUN_INSTALL_CACHE_DIR: join(cache, "bun"),
|
|
7113
|
-
PIP_CACHE_DIR: join(cache, "pip"),
|
|
7114
|
-
UV_CACHE_DIR: join(cache, "uv"),
|
|
7115
|
-
GOMODCACHE: join(cache, "gomod"),
|
|
7116
|
-
GOCACHE: join(cache, "gobuild"),
|
|
7117
|
-
CARGO_HOME: join(cache, "cargo")
|
|
7118
|
-
};
|
|
7119
|
-
}
|
|
7120
|
-
function wrap(profile, cmd, args) {
|
|
7121
|
-
if (profile === null)
|
|
7122
|
-
return { cmd, args };
|
|
7123
|
-
return { cmd: SANDBOX_EXEC, args: ["-p", profile, cmd, ...args] };
|
|
7124
|
-
}
|
|
7125
|
-
function probeSandbox(opts) {
|
|
7126
|
-
const outside = join(homedir(), `.firegoat-escape-probe-${process.pid}-${Date.now()}`);
|
|
7127
|
-
const script = 'touch "$1" 2>/dev/null && echo home-write; cat "$2" >/dev/null 2>&1 && echo repo-read; touch "$3" 2>/dev/null && echo work-write; rm -f "$1" "$3"';
|
|
7128
|
-
const { cmd, args } = wrap(opts.profile, "/bin/sh", [
|
|
7129
|
-
"-c",
|
|
7130
|
-
script,
|
|
7131
|
-
"probe",
|
|
7132
|
-
outside,
|
|
7133
|
-
opts.repoFile,
|
|
7134
|
-
join(opts.workdir, ".firegoat-probe")
|
|
7135
|
-
]);
|
|
7136
|
-
const r = spawnSync(cmd, args, { encoding: "utf8", cwd: opts.workdir });
|
|
7137
|
-
if (r.error)
|
|
7138
|
-
return `sandbox probe could not start: ${r.error.message}`;
|
|
7139
|
-
const got = r.stdout.trim().split(`
|
|
7140
|
-
`).filter(Boolean);
|
|
7141
|
-
if (got.includes("home-write") || got.includes("repo-read"))
|
|
7142
|
-
return `sandbox does not hold (escaped: ${got.filter((g) => g !== "work-write").join(", ")}); refusing to run the agent`;
|
|
7143
|
-
if (!got.includes("work-write"))
|
|
7144
|
-
return `sandbox blocks writes in the workdir (stderr: ${r.stderr.trim() || "none"})`;
|
|
7145
|
-
return;
|
|
7146
|
-
}
|
|
7147
|
-
|
|
7148
|
-
// src/auth.ts
|
|
7149
|
-
var authFile = () => join2(firegoatHome(), "auth.json");
|
|
7150
|
-
var keyFile = () => join2(firegoatHome(), "anthropic-api-key");
|
|
7151
|
-
|
|
7152
|
-
class AuthError extends Error {
|
|
7153
|
-
}
|
|
7154
|
-
function readMethod() {
|
|
7155
|
-
const file = authFile();
|
|
7156
|
-
if (!existsSync2(file))
|
|
7157
|
-
return;
|
|
7158
|
-
let doc;
|
|
7159
|
-
try {
|
|
7160
|
-
doc = JSON.parse(readFileSync(file, "utf8"));
|
|
7161
|
-
} catch (e) {
|
|
7162
|
-
throw new AuthError(`${file} is not valid JSON: ${e.message}`);
|
|
7163
|
-
}
|
|
7164
|
-
const method = doc?.method;
|
|
7165
|
-
if (method !== "claude" && method !== "api-key")
|
|
7166
|
-
throw new AuthError(`${file}: method must be "claude" or "api-key", got ${JSON.stringify(method)}. Run: npx firegoat init`);
|
|
7167
|
-
return method;
|
|
7168
|
-
}
|
|
7169
|
-
function saveMethod(method) {
|
|
7170
|
-
mkdirSync2(firegoatHome(), { recursive: true });
|
|
7171
|
-
writeFileSync(authFile(), `${JSON.stringify({ method }, null, 2)}
|
|
7172
|
-
`);
|
|
7173
|
-
}
|
|
7174
|
-
function saveApiKey(apiKey) {
|
|
7175
|
-
mkdirSync2(firegoatHome(), { recursive: true });
|
|
7176
|
-
const file = keyFile();
|
|
7177
|
-
writeFileSync(file, `${apiKey}
|
|
7178
|
-
`, { mode: 384 });
|
|
7179
|
-
chmodSync(file, 384);
|
|
7180
|
-
saveMethod("api-key");
|
|
7181
|
-
return file;
|
|
7182
|
-
}
|
|
7183
|
-
function looksLikeAnthropicKey(s) {
|
|
7184
|
-
return /^sk-ant-[A-Za-z0-9_-]{20,}$/.test(s);
|
|
7185
|
-
}
|
|
7186
|
-
function resolveAuth(env) {
|
|
7187
|
-
if (env.CI && env.ANTHROPIC_API_KEY)
|
|
7188
|
-
return {
|
|
7189
|
-
method: "api-key",
|
|
7190
|
-
label: "Anthropic API key from ANTHROPIC_API_KEY (CI)",
|
|
7191
|
-
apiKey: env.ANTHROPIC_API_KEY
|
|
7192
|
-
};
|
|
7193
|
-
const method = readMethod() ?? "claude";
|
|
7194
|
-
if (method === "claude")
|
|
7195
|
-
return {
|
|
7196
|
-
method,
|
|
7197
|
-
label: `Claude login in ${claudeConfigDir()}`
|
|
7198
|
-
};
|
|
7199
|
-
const file = keyFile();
|
|
7200
|
-
if (!existsSync2(file))
|
|
7201
|
-
throw new AuthError(`auth is set to api-key but ${file} is missing. Run: npx firegoat init`);
|
|
7202
|
-
const apiKey = readFileSync(file, "utf8").trim();
|
|
7203
|
-
if (!apiKey)
|
|
7204
|
-
throw new AuthError(`${file} is empty. Run: npx firegoat init`);
|
|
7205
|
-
return { method, label: `Anthropic API key in ${file}`, apiKey };
|
|
7206
|
-
}
|
|
7207
|
-
|
|
7208
|
-
// src/exit.ts
|
|
7209
|
-
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7210
|
-
|
|
7211
|
-
// src/help.ts
|
|
7212
|
-
var EXIT_CODES = `Exit codes:
|
|
7213
|
-
0 every run's script checks passed
|
|
7214
|
-
1 a script check failed
|
|
7215
|
-
2 at least one run was not graded (claude missing or logged out, crash, timeout,
|
|
7216
|
-
setup failed); wins over 1
|
|
7217
|
-
3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)`;
|
|
7218
|
-
function topHelp(version) {
|
|
7219
|
-
return `firegoat ${version}: local AX evals. A coding agent gets one prompt in a fresh,
|
|
7220
|
-
sandboxed folder; firegoat records the transcript and runs your script checks.
|
|
7221
|
-
|
|
7222
|
-
Typical flow (for an agent driving firegoat on behalf of a human):
|
|
7223
|
-
1. npx firegoat init --yes write firegoat.yaml (read: npx firegoat init --help)
|
|
7224
|
-
2. edit firegoat.yaml real user problems as prompts, 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
|
-
|
|
7353
|
-
// src/claude-login.ts
|
|
7354
|
-
function claudeLoginCommand(args) {
|
|
7355
|
-
let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
|
|
7356
|
-
for (let i = 0;i < args.length; i++) {
|
|
7357
|
-
const a = args[i];
|
|
7358
|
-
if (a === "--help" || a === "-h") {
|
|
7359
|
-
console.log(CLAUDE_LOGIN_HELP);
|
|
7360
|
-
return EXIT.pass;
|
|
7361
|
-
}
|
|
7362
|
-
if (a === "--claude-bin" && args[i + 1] !== undefined)
|
|
7363
|
-
claudeBin = args[++i];
|
|
7364
|
-
else if (a.startsWith("--claude-bin="))
|
|
7365
|
-
claudeBin = a.slice(13);
|
|
7366
|
-
else {
|
|
7367
|
-
console.error(`firegoat claude login: unknown argument: ${a}`);
|
|
7368
|
-
return EXIT.usage;
|
|
7369
|
-
}
|
|
7370
|
-
}
|
|
7371
|
-
const configDir = claudeConfigDir();
|
|
7372
|
-
mkdirSync3(configDir, { recursive: true });
|
|
7373
|
-
const env = {
|
|
7374
|
-
...process.env,
|
|
7375
|
-
CLAUDE_CONFIG_DIR: configDir
|
|
7376
|
-
};
|
|
7377
|
-
delete env.ANTHROPIC_API_KEY;
|
|
7378
|
-
console.error(`Signing claude in to ${configDir}. Evals run with this login, never your own ~/.claude.`);
|
|
7379
|
-
const r = spawnSync2(claudeBin, ["auth", "login"], { env, stdio: "inherit" });
|
|
7380
|
-
if (r.error) {
|
|
7381
|
-
console.error(`firegoat claude login: could not run ${claudeBin}: ${r.error.message}`);
|
|
7382
|
-
return EXIT.notGraded;
|
|
7383
|
-
}
|
|
7384
|
-
if (r.status !== 0) {
|
|
7385
|
-
console.error(`firegoat claude login: claude auth login exited ${r.status}`);
|
|
7386
|
-
return EXIT.notGraded;
|
|
7387
|
-
}
|
|
7388
|
-
saveMethod("claude");
|
|
7389
|
-
console.log("logged in; evals now sign in with this Claude login. next: npx firegoat eval");
|
|
7390
|
-
return EXIT.pass;
|
|
7391
|
-
}
|
|
7392
|
-
|
|
7393
6988
|
// src/config.ts
|
|
7394
|
-
import { existsSync
|
|
7395
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
6989
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7396
6990
|
|
|
7397
6991
|
// node_modules/yaml/dist/index.js
|
|
7398
6992
|
var composer = require_composer();
|
|
@@ -7443,7 +7037,6 @@ var $visitAsync = visit.visitAsync;
|
|
|
7443
7037
|
// src/config.ts
|
|
7444
7038
|
var CONFIG_FILE = "firegoat.yaml";
|
|
7445
7039
|
var DEFAULT_TIMEOUT = 600;
|
|
7446
|
-
var SUPPORTED_AGENTS = ["claude-code"];
|
|
7447
7040
|
|
|
7448
7041
|
class ConfigError extends Error {
|
|
7449
7042
|
}
|
|
@@ -7465,7 +7058,12 @@ function posInt(v, path, fallback) {
|
|
|
7465
7058
|
fail(path, "must be a positive integer");
|
|
7466
7059
|
return v;
|
|
7467
7060
|
}
|
|
7468
|
-
function
|
|
7061
|
+
function onlyKeys(v, path, allowed) {
|
|
7062
|
+
for (const k of Object.keys(v))
|
|
7063
|
+
if (!allowed.includes(k))
|
|
7064
|
+
fail(path === "(root)" ? k : `${path}.${k}`, `unknown key (allowed: ${allowed.join(", ")})`);
|
|
7065
|
+
}
|
|
7066
|
+
function parseConfig(text) {
|
|
7469
7067
|
let raw;
|
|
7470
7068
|
try {
|
|
7471
7069
|
raw = $parse(text);
|
|
@@ -7474,17 +7072,8 @@ function parseConfig(text, warn = console.error) {
|
|
|
7474
7072
|
}
|
|
7475
7073
|
if (!isRecord(raw))
|
|
7476
7074
|
fail("(root)", "must be a mapping");
|
|
7477
|
-
|
|
7478
|
-
if (!Array.isArray(agents) || agents.length === 0)
|
|
7479
|
-
fail("agents", "must be a non-empty list");
|
|
7480
|
-
agents.forEach((a, i) => {
|
|
7481
|
-
if (!SUPPORTED_AGENTS.includes(a)) {
|
|
7482
|
-
fail(`agents[${i}]`, `unsupported agent ${JSON.stringify(a)} (v1 supports: ${SUPPORTED_AGENTS.join(", ")})`);
|
|
7483
|
-
}
|
|
7484
|
-
});
|
|
7075
|
+
onlyKeys(raw, "(root)", ["runs", "setup", "workdir_setup", "vars", "tests"]);
|
|
7485
7076
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7486
|
-
if (raw.build !== undefined)
|
|
7487
|
-
fail("build", "was replaced by setup: in firegoat 0.4.0: a shell command (or list) that writes what the agent gets to $FIREGOAT_OUT");
|
|
7488
7077
|
const setup = commands(raw.setup, "setup");
|
|
7489
7078
|
const workdirSetup = commands(raw.workdir_setup, "workdir_setup");
|
|
7490
7079
|
const vars = parseVars(raw.vars);
|
|
@@ -7495,6 +7084,7 @@ function parseConfig(text, warn = console.error) {
|
|
|
7495
7084
|
const p = `tests[${i}]`;
|
|
7496
7085
|
if (!isRecord(t))
|
|
7497
7086
|
fail(p, "must be a mapping");
|
|
7087
|
+
onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout"]);
|
|
7498
7088
|
const name = str(t.name, `${p}.name`);
|
|
7499
7089
|
if (!NAME.test(name))
|
|
7500
7090
|
fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
|
|
@@ -7509,27 +7099,18 @@ function parseConfig(text, warn = console.error) {
|
|
|
7509
7099
|
const cp = `${p}.checks[${j}]`;
|
|
7510
7100
|
if (!isRecord(c))
|
|
7511
7101
|
fail(cp, "must be a mapping");
|
|
7512
|
-
|
|
7513
|
-
fail(`${cp}.judge`, "plain-language checks were removed; put the criteria in end_goal (your agent judges it) or write a run: check");
|
|
7102
|
+
onlyKeys(c, cp, ["run"]);
|
|
7514
7103
|
checks.push({ run: str(c.run, `${cp}.run`) });
|
|
7515
7104
|
});
|
|
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";
|
|
7523
7105
|
return {
|
|
7524
7106
|
name,
|
|
7525
7107
|
prompt: str(t.prompt, `${p}.prompt`),
|
|
7526
|
-
end_goal: str(t
|
|
7108
|
+
end_goal: str(t.end_goal, `${p}.end_goal`),
|
|
7527
7109
|
checks,
|
|
7528
7110
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
|
|
7529
7111
|
};
|
|
7530
7112
|
});
|
|
7531
7113
|
return {
|
|
7532
|
-
agents,
|
|
7533
7114
|
runs,
|
|
7534
7115
|
setup,
|
|
7535
7116
|
workdirSetup,
|
|
@@ -7575,260 +7156,134 @@ function render(text, vars) {
|
|
|
7575
7156
|
return v;
|
|
7576
7157
|
});
|
|
7577
7158
|
}
|
|
7578
|
-
function loadConfig(dir
|
|
7159
|
+
function loadConfig(dir) {
|
|
7579
7160
|
const file = `${dir}/${CONFIG_FILE}`;
|
|
7580
|
-
if (!
|
|
7161
|
+
if (!existsSync(file))
|
|
7581
7162
|
throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
|
|
7582
|
-
return parseConfig(
|
|
7583
|
-
}
|
|
7584
|
-
function findConfigDir(cwd) {
|
|
7585
|
-
let dir = cwd;
|
|
7586
|
-
for (;; ) {
|
|
7587
|
-
if (existsSync3(join3(dir, CONFIG_FILE)))
|
|
7588
|
-
return dir;
|
|
7589
|
-
const parent = dirname2(dir);
|
|
7590
|
-
if (existsSync3(join3(dir, ".git")) || parent === dir)
|
|
7591
|
-
return cwd;
|
|
7592
|
-
dir = parent;
|
|
7593
|
-
}
|
|
7163
|
+
return parseConfig(readFileSync(file, "utf8"));
|
|
7594
7164
|
}
|
|
7595
7165
|
|
|
7596
7166
|
// src/eval.ts
|
|
7597
7167
|
import { randomBytes } from "node:crypto";
|
|
7598
7168
|
import {
|
|
7599
|
-
createWriteStream as
|
|
7600
|
-
existsSync as
|
|
7601
|
-
mkdirSync as
|
|
7602
|
-
|
|
7603
|
-
readFileSync as readFileSync3,
|
|
7604
|
-
realpathSync as realpathSync2,
|
|
7169
|
+
createWriteStream as createWriteStream2,
|
|
7170
|
+
existsSync as existsSync3,
|
|
7171
|
+
mkdirSync as mkdirSync2,
|
|
7172
|
+
readdirSync,
|
|
7605
7173
|
writeFileSync as writeFileSync2
|
|
7606
7174
|
} from "node:fs";
|
|
7607
|
-
import {
|
|
7608
|
-
import { join as join6, resolve as resolve2 } from "node:path";
|
|
7175
|
+
import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
7609
7176
|
|
|
7610
7177
|
// src/claude.ts
|
|
7611
|
-
import { spawn, spawnSync
|
|
7612
|
-
import { createWriteStream } from "node:fs";
|
|
7613
|
-
import { createInterface } from "node:readline";
|
|
7178
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7614
7179
|
var CLAUDE_FLAGS = [
|
|
7615
7180
|
"--output-format",
|
|
7616
7181
|
"stream-json",
|
|
7617
7182
|
"--verbose",
|
|
7618
7183
|
"--dangerously-skip-permissions",
|
|
7619
|
-
"--no-session-persistence",
|
|
7620
7184
|
"--strict-mcp-config",
|
|
7621
7185
|
"--setting-sources",
|
|
7622
|
-
""
|
|
7186
|
+
"",
|
|
7187
|
+
"--disable-slash-commands"
|
|
7623
7188
|
];
|
|
7624
|
-
function
|
|
7625
|
-
return
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
return;
|
|
7631
|
-
try {
|
|
7632
|
-
process.kill(-child.pid, signal);
|
|
7633
|
-
} catch {
|
|
7634
|
-
try {
|
|
7635
|
-
child.kill(signal);
|
|
7636
|
-
} catch {}
|
|
7637
|
-
}
|
|
7638
|
-
}
|
|
7639
|
-
var signalsHooked = false;
|
|
7640
|
-
function hookSignals() {
|
|
7641
|
-
if (signalsHooked)
|
|
7642
|
-
return;
|
|
7643
|
-
signalsHooked = true;
|
|
7644
|
-
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
7645
|
-
process.on(sig, () => {
|
|
7646
|
-
for (const c of live)
|
|
7647
|
-
killGroup(c, "SIGKILL");
|
|
7648
|
-
process.exit(sig === "SIGINT" ? 130 : 143);
|
|
7649
|
-
});
|
|
7650
|
-
}
|
|
7189
|
+
function claudeEnv(configDir) {
|
|
7190
|
+
return {
|
|
7191
|
+
...process.env,
|
|
7192
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
7193
|
+
CLAUDE_SECURESTORAGE_CONFIG_DIR: ""
|
|
7194
|
+
};
|
|
7651
7195
|
}
|
|
7652
|
-
function
|
|
7653
|
-
hookSignals();
|
|
7196
|
+
function run(opts) {
|
|
7654
7197
|
return new Promise((resolve) => {
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
7661
|
-
detached: true,
|
|
7662
|
-
shell: false
|
|
7663
|
-
});
|
|
7664
|
-
} catch (e) {
|
|
7665
|
-
resolve({
|
|
7666
|
-
code: null,
|
|
7667
|
-
timedOut: false,
|
|
7668
|
-
spawnError: e
|
|
7669
|
-
});
|
|
7670
|
-
return;
|
|
7671
|
-
}
|
|
7672
|
-
live.add(child);
|
|
7198
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
7199
|
+
cwd: opts.cwd,
|
|
7200
|
+
env: opts.env,
|
|
7201
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7202
|
+
});
|
|
7673
7203
|
let timedOut = false;
|
|
7674
|
-
let
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
const finish = (r) => {
|
|
7682
|
-
if (done)
|
|
7204
|
+
let settled = false;
|
|
7205
|
+
const timer = setTimeout(() => {
|
|
7206
|
+
timedOut = true;
|
|
7207
|
+
child.kill("SIGKILL");
|
|
7208
|
+
}, opts.timeoutMs);
|
|
7209
|
+
const done = (r) => {
|
|
7210
|
+
if (settled)
|
|
7683
7211
|
return;
|
|
7684
|
-
|
|
7212
|
+
settled = true;
|
|
7685
7213
|
clearTimeout(timer);
|
|
7686
|
-
clearTimeout(killTimer);
|
|
7687
|
-
clearTimeout(graceTimer);
|
|
7688
|
-
live.delete(child);
|
|
7689
7214
|
resolve(r);
|
|
7690
7215
|
};
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
};
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
};
|
|
7700
|
-
const timer = setTimeout(() => {
|
|
7701
|
-
timedOut = true;
|
|
7702
|
-
stop();
|
|
7703
|
-
}, opts.timeoutMs);
|
|
7704
|
-
child.on("error", (err) => {
|
|
7705
|
-
if (child.pid === undefined)
|
|
7706
|
-
finish({ code: null, timedOut: false, spawnError: err });
|
|
7707
|
-
});
|
|
7708
|
-
child.on("exit", (code) => {
|
|
7709
|
-
exited = true;
|
|
7710
|
-
exitCode = code;
|
|
7711
|
-
graceTimer = setTimeout(() => {
|
|
7712
|
-
child.stdout?.destroy();
|
|
7713
|
-
child.stderr?.destroy();
|
|
7714
|
-
finish({ code: exitCode, timedOut });
|
|
7715
|
-
}, 2000);
|
|
7716
|
-
maybeFinish();
|
|
7717
|
-
});
|
|
7718
|
-
child.stderr?.on("data", opts.onStderr);
|
|
7719
|
-
child.stderr?.on("close", () => {
|
|
7720
|
-
stderrDone = true;
|
|
7721
|
-
maybeFinish();
|
|
7722
|
-
});
|
|
7723
|
-
if (opts.onLine) {
|
|
7724
|
-
const onLine = opts.onLine;
|
|
7725
|
-
const rl = createInterface({
|
|
7726
|
-
input: child.stdout
|
|
7727
|
-
});
|
|
7728
|
-
rl.on("line", (line) => {
|
|
7729
|
-
if (onLine(line) === "kill")
|
|
7730
|
-
stop();
|
|
7731
|
-
});
|
|
7732
|
-
rl.on("close", () => {
|
|
7733
|
-
linesDone = true;
|
|
7734
|
-
maybeFinish();
|
|
7735
|
-
});
|
|
7736
|
-
} else {
|
|
7737
|
-
if (opts.onStdout)
|
|
7738
|
-
child.stdout?.on("data", opts.onStdout);
|
|
7739
|
-
child.stdout?.on("close", () => {
|
|
7740
|
-
stdoutDone = true;
|
|
7741
|
-
maybeFinish();
|
|
7742
|
-
});
|
|
7743
|
-
}
|
|
7216
|
+
child.stdout.on("data", opts.onStdout);
|
|
7217
|
+
child.stderr.on("data", opts.onStderr);
|
|
7218
|
+
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
7219
|
+
child.on("close", (code) => done({ code, timedOut }));
|
|
7220
|
+
child.on("exit", (code) => setTimeout(() => {
|
|
7221
|
+
child.stdout.destroy();
|
|
7222
|
+
child.stderr.destroy();
|
|
7223
|
+
done({ code, timedOut });
|
|
7224
|
+
}, 2000).unref());
|
|
7744
7225
|
});
|
|
7745
7226
|
}
|
|
7746
|
-
function closeStream(s) {
|
|
7747
|
-
return new Promise((r) => s.end(r));
|
|
7748
|
-
}
|
|
7749
7227
|
async function runClaude(opts) {
|
|
7750
|
-
|
|
7751
|
-
const
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
let result;
|
|
7755
|
-
const spawned = await spawnWatched({
|
|
7756
|
-
...wrap(opts.profile, opts.bin, claudeArgv(opts.prompt)),
|
|
7228
|
+
let stdout = "";
|
|
7229
|
+
const exited = await run({
|
|
7230
|
+
cmd: opts.bin,
|
|
7231
|
+
args: ["-p", opts.prompt, ...CLAUDE_FLAGS],
|
|
7757
7232
|
cwd: opts.cwd,
|
|
7758
7233
|
env: opts.env,
|
|
7759
7234
|
timeoutMs: opts.timeoutMs,
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
if (line.trim() === "")
|
|
7765
|
-
return;
|
|
7766
|
-
let msg;
|
|
7767
|
-
try {
|
|
7768
|
-
const parsed = JSON.parse(line);
|
|
7769
|
-
if (typeof parsed === "object" && parsed !== null)
|
|
7770
|
-
msg = parsed;
|
|
7771
|
-
} catch {}
|
|
7772
|
-
if (first) {
|
|
7773
|
-
first = false;
|
|
7774
|
-
if (msg?.type !== "system" || msg.subtype !== "init") {
|
|
7775
|
-
badStart = `claude output did not start with a system/init stream-json line (got: ${line.slice(0, 120)})`;
|
|
7776
|
-
return "kill";
|
|
7777
|
-
}
|
|
7778
|
-
if (msg.apiKeySource !== opts.keySource) {
|
|
7779
|
-
badStart = opts.keySource === "none" ? `claude would use API key billing (apiKeySource=${JSON.stringify(msg.apiKeySource)}) instead of your Claude login. firegoat removes ANTHROPIC_API_KEY from the agent's env; check apiKeyHelper or other API key settings, then run \`npx firegoat claude login\`` : `claude did not use the configured API key (apiKeySource=${JSON.stringify(msg.apiKeySource)}, expected "ANTHROPIC_API_KEY")`;
|
|
7780
|
-
return "kill";
|
|
7781
|
-
}
|
|
7782
|
-
return;
|
|
7783
|
-
}
|
|
7784
|
-
if (msg?.type === "result")
|
|
7785
|
-
result = msg;
|
|
7786
|
-
return;
|
|
7787
|
-
}
|
|
7235
|
+
onStdout: (c) => {
|
|
7236
|
+
stdout += c.toString();
|
|
7237
|
+
},
|
|
7238
|
+
onStderr: (c) => opts.stderr.write(c)
|
|
7788
7239
|
});
|
|
7789
|
-
|
|
7790
|
-
if (spawned.spawnError) {
|
|
7791
|
-
const e = spawned.spawnError;
|
|
7792
|
-
if (e.code === "ENOENT")
|
|
7793
|
-
return {
|
|
7794
|
-
ok: false,
|
|
7795
|
-
reason: `claude binary not found: ${opts.bin} (install Claude Code, or pass --claude-bin / set FIREGOAT_CLAUDE_BIN)`
|
|
7796
|
-
};
|
|
7240
|
+
if (exited.error)
|
|
7797
7241
|
return {
|
|
7798
7242
|
ok: false,
|
|
7799
|
-
reason: `could not start ${opts.bin}: ${
|
|
7243
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`
|
|
7800
7244
|
};
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7245
|
+
const events = stdout.split(`
|
|
7246
|
+
`).filter((l) => l.trim() !== "").map((l) => {
|
|
7247
|
+
try {
|
|
7248
|
+
return JSON.parse(l);
|
|
7249
|
+
} catch {
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
});
|
|
7253
|
+
const init = events[0];
|
|
7254
|
+
if (init?.type !== "system" || init.subtype !== "init")
|
|
7805
7255
|
return {
|
|
7806
7256
|
ok: false,
|
|
7807
|
-
reason: `
|
|
7808
|
-
result
|
|
7257
|
+
reason: `claude did not start a session (exit ${exited.code ?? "by signal"}), see stderr.txt`
|
|
7809
7258
|
};
|
|
7810
|
-
|
|
7259
|
+
const sessionId = String(init.session_id);
|
|
7260
|
+
const result = events.findLast((e) => e?.type === "result");
|
|
7261
|
+
if (exited.timedOut)
|
|
7811
7262
|
return {
|
|
7812
7263
|
ok: false,
|
|
7813
|
-
reason: `
|
|
7264
|
+
reason: `timed out after ${opts.timeoutMs / 1000}s`,
|
|
7265
|
+
result,
|
|
7266
|
+
sessionId
|
|
7814
7267
|
};
|
|
7815
7268
|
if (!result)
|
|
7816
7269
|
return {
|
|
7817
7270
|
ok: false,
|
|
7818
|
-
reason: `claude exited ${
|
|
7271
|
+
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7272
|
+
sessionId
|
|
7819
7273
|
};
|
|
7820
7274
|
if (result.is_error === true || result.subtype !== "success")
|
|
7821
7275
|
return {
|
|
7822
7276
|
ok: false,
|
|
7823
|
-
reason: `claude result is an error (subtype=${result.subtype}
|
|
7824
|
-
result
|
|
7277
|
+
reason: `claude result is an error (subtype=${result.subtype})`,
|
|
7278
|
+
result,
|
|
7279
|
+
sessionId
|
|
7825
7280
|
};
|
|
7826
|
-
return { ok: true, result };
|
|
7281
|
+
return { ok: true, result, sessionId };
|
|
7827
7282
|
}
|
|
7828
7283
|
async function runShell(opts) {
|
|
7829
7284
|
opts.log.write(`$ ${opts.run}
|
|
7830
7285
|
`);
|
|
7831
|
-
const r = await
|
|
7286
|
+
const r = await run({
|
|
7832
7287
|
cmd: "/bin/sh",
|
|
7833
7288
|
args: ["-c", opts.run],
|
|
7834
7289
|
cwd: opts.cwd,
|
|
@@ -7838,8 +7293,8 @@ async function runShell(opts) {
|
|
|
7838
7293
|
onStderr: (c) => opts.log.write(c)
|
|
7839
7294
|
});
|
|
7840
7295
|
let error;
|
|
7841
|
-
if (r.
|
|
7842
|
-
error = `could not start /bin/sh: ${r.
|
|
7296
|
+
if (r.error)
|
|
7297
|
+
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7843
7298
|
else if (r.timedOut)
|
|
7844
7299
|
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7845
7300
|
else if (r.code === null)
|
|
@@ -7847,23 +7302,17 @@ async function runShell(opts) {
|
|
|
7847
7302
|
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7848
7303
|
|
|
7849
7304
|
`);
|
|
7850
|
-
|
|
7851
|
-
if (error)
|
|
7852
|
-
out.error = error;
|
|
7853
|
-
if (r.spawnError)
|
|
7854
|
-
out.notStarted = true;
|
|
7855
|
-
return out;
|
|
7305
|
+
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7856
7306
|
}
|
|
7857
7307
|
function checkLogin(opts) {
|
|
7858
|
-
const
|
|
7859
|
-
const r = spawnSync3(cmd, args, {
|
|
7308
|
+
const r = spawnSync(opts.bin, ["auth", "status"], {
|
|
7860
7309
|
cwd: opts.cwd,
|
|
7861
7310
|
env: opts.env,
|
|
7862
7311
|
encoding: "utf8",
|
|
7863
7312
|
timeout: 30000
|
|
7864
7313
|
});
|
|
7865
7314
|
if (r.error)
|
|
7866
|
-
return `could not run
|
|
7315
|
+
return `could not run ${opts.bin}: ${r.error.message}`;
|
|
7867
7316
|
let status;
|
|
7868
7317
|
try {
|
|
7869
7318
|
status = JSON.parse(r.stdout);
|
|
@@ -7871,56 +7320,163 @@ function checkLogin(opts) {
|
|
|
7871
7320
|
if (status?.loggedIn === true)
|
|
7872
7321
|
return;
|
|
7873
7322
|
if (status?.loggedIn === false)
|
|
7874
|
-
return
|
|
7323
|
+
return "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again";
|
|
7875
7324
|
return `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`;
|
|
7876
7325
|
}
|
|
7877
7326
|
|
|
7327
|
+
// src/exit.ts
|
|
7328
|
+
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7329
|
+
|
|
7330
|
+
// src/firegoat-dir.ts
|
|
7331
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
|
|
7332
|
+
import { join } from "node:path";
|
|
7333
|
+
function ensureFiregoatDir(root) {
|
|
7334
|
+
const dir = join(root, ".firegoat");
|
|
7335
|
+
mkdirSync(dir, { recursive: true });
|
|
7336
|
+
const ignore = join(dir, ".gitignore");
|
|
7337
|
+
if (!existsSync2(ignore))
|
|
7338
|
+
writeFileSync(ignore, `*
|
|
7339
|
+
`);
|
|
7340
|
+
return dir;
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
// src/help.ts
|
|
7344
|
+
var EXIT_CODES = `Exit codes:
|
|
7345
|
+
0 every run's script checks passed
|
|
7346
|
+
1 a script check failed
|
|
7347
|
+
2 at least one run was not graded (claude missing or logged out, crash, timeout,
|
|
7348
|
+
setup failed); wins over 1
|
|
7349
|
+
3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)`;
|
|
7350
|
+
function topHelp(version) {
|
|
7351
|
+
return `firegoat ${version}: local AX evals. Your own claude gets one prompt in a fresh
|
|
7352
|
+
folder, with none of your claude config; firegoat saves the session and runs your
|
|
7353
|
+
script checks.
|
|
7354
|
+
|
|
7355
|
+
npx firegoat init create firegoat.yaml and .firegoat/, install the skills
|
|
7356
|
+
npx firegoat eval run the tests (claude must be logged in: run \`claude\` once)
|
|
7357
|
+
|
|
7358
|
+
Typical flow for a coding agent driving firegoat:
|
|
7359
|
+
1. read npx firegoat init --help, then edit firegoat.yaml
|
|
7360
|
+
2. npx firegoat eval --json
|
|
7361
|
+
3. judge each run's end_goal from its run.json, session and workdir: firegoat only
|
|
7362
|
+
runs the script checks
|
|
7363
|
+
|
|
7364
|
+
Run firegoat <command> --help for details and examples.
|
|
7365
|
+
|
|
7366
|
+
${EXIT_CODES}`;
|
|
7367
|
+
}
|
|
7368
|
+
var INIT_HELP = `Usage: firegoat init [--force] [--no-skills]
|
|
7369
|
+
|
|
7370
|
+
In the current folder: creates .firegoat/ (ignored by git through its own .gitignore)
|
|
7371
|
+
and a starter firegoat.yaml, installs the firegoat-onboarding and
|
|
7372
|
+
suggesting-eval-prompts skills into .claude/skills/ (npx skills add firegoat-ai/skills;
|
|
7373
|
+
asks first in a terminal), then prints a prompt to paste into your coding agent.
|
|
7374
|
+
|
|
7375
|
+
--force replace an existing firegoat.yaml
|
|
7376
|
+
--no-skills don't install the skills
|
|
7377
|
+
|
|
7378
|
+
firegoat.yaml reference:
|
|
7379
|
+
runs: 1 runs per test
|
|
7380
|
+
setup: <sh> | [<sh>, ...] once per eval, in this folder; what it writes to
|
|
7381
|
+
$FIREGOAT_OUT is first on the agent's PATH
|
|
7382
|
+
workdir_setup: <sh> | [...] in each fresh folder before the agent starts
|
|
7383
|
+
vars: {KEY: value} defaults for {{vars.KEY}} in prompts and checks;
|
|
7384
|
+
--var KEY=value overrides
|
|
7385
|
+
tests:
|
|
7386
|
+
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
7387
|
+
prompt: ... what a user would ask the agent; one concrete outcome
|
|
7388
|
+
end_goal: ... what is true when the problem is solved; not graded by
|
|
7389
|
+
firegoat, handed to you to judge
|
|
7390
|
+
checks: optional shell commands, run in the agent's folder
|
|
7391
|
+
- run: ... afterwards; exit 0 = pass; they set eval's exit code
|
|
7392
|
+
timeout: 600 optional, seconds
|
|
7393
|
+
|
|
7394
|
+
Unknown keys are errors. setup, workdir_setup and checks get the vars as env vars,
|
|
7395
|
+
plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; checks also get $FIREGOAT_SESSION (the
|
|
7396
|
+
claude session file, one JSON event per line).
|
|
7397
|
+
|
|
7398
|
+
Example:
|
|
7399
|
+
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7400
|
+
workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7401
|
+
tests:
|
|
7402
|
+
- name: first-call
|
|
7403
|
+
prompt: >-
|
|
7404
|
+
Using the acme SDK (already installed from the build under test), send a test
|
|
7405
|
+
event and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
|
|
7406
|
+
end_goal: >-
|
|
7407
|
+
A script sends one event with the SDK's client (not raw HTTP) and prints its
|
|
7408
|
+
id, without installing acme from the registry.
|
|
7409
|
+
checks:
|
|
7410
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_SESSION"
|
|
7411
|
+
- run: |
|
|
7412
|
+
! grep -Eq 'npm (install|i) [^"]*acme' "$FIREGOAT_SESSION"
|
|
7413
|
+
|
|
7414
|
+
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7415
|
+
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json]
|
|
7416
|
+
[--claude-bin PATH]
|
|
7417
|
+
|
|
7418
|
+
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
7419
|
+
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with setup:'s
|
|
7420
|
+
outputs on PATH. Then the test's checks run in that folder.
|
|
7421
|
+
|
|
7422
|
+
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
7423
|
+
with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
|
|
7424
|
+
plugins, skills or MCP servers. claude saves its session there. There is no
|
|
7425
|
+
sandbox: the agent can reach the rest of your machine, including the repo.
|
|
7426
|
+
|
|
7427
|
+
test.. only these tests (by name)
|
|
7428
|
+
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7429
|
+
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7430
|
+
--json the summary as JSON on stdout (or {error, exitCode} if eval
|
|
7431
|
+
stops early); progress goes to stderr
|
|
7432
|
+
--claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
|
|
7433
|
+
|
|
7434
|
+
Output: .firegoat/runs/<runId>/
|
|
7435
|
+
summary.json every run, plus totals
|
|
7436
|
+
<test>/<n>/run.json one run: status and reason (from the checks), prompt,
|
|
7437
|
+
end_goal, checks [{run, code}], and paths: workdir,
|
|
7438
|
+
session (claude's session file), stderr, checksLog
|
|
7439
|
+
setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
|
|
7440
|
+
|
|
7441
|
+
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
7442
|
+
yours to judge. The session is one JSON event per line: "assistant" messages hold
|
|
7443
|
+
"tool_use" items (the agent's commands), "user" messages their output.
|
|
7444
|
+
|
|
7445
|
+
Examples:
|
|
7446
|
+
npx firegoat eval --json all tests, JSON summary on stdout
|
|
7447
|
+
npx firegoat eval quickstart --runs 3 one test, three runs
|
|
7448
|
+
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
7449
|
+
|
|
7450
|
+
A run can take minutes (default timeout 600s per test).
|
|
7451
|
+
|
|
7452
|
+
${EXIT_CODES}`;
|
|
7453
|
+
|
|
7878
7454
|
// src/setup.ts
|
|
7879
|
-
import {
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
const log = createWriteStream2(logPath);
|
|
7455
|
+
import { createWriteStream } from "node:fs";
|
|
7456
|
+
var STEP_TIMEOUT_MS = 600000;
|
|
7457
|
+
async function runSteps(opts) {
|
|
7458
|
+
const log = createWriteStream(opts.logPath);
|
|
7884
7459
|
try {
|
|
7885
|
-
for (const run of commands) {
|
|
7460
|
+
for (const run of opts.commands) {
|
|
7886
7461
|
const r = await runShell({
|
|
7887
7462
|
run,
|
|
7888
|
-
cwd,
|
|
7889
|
-
env,
|
|
7890
|
-
timeoutMs:
|
|
7463
|
+
cwd: opts.cwd,
|
|
7464
|
+
env: opts.env,
|
|
7465
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
7891
7466
|
log
|
|
7892
7467
|
});
|
|
7893
7468
|
if (r.code !== 0)
|
|
7894
|
-
return `${what} failed (${r.error ?? `exit ${r.code}`}): ${run} (log: ${logPath})`;
|
|
7469
|
+
return `${opts.what} failed (${r.error ?? `exit ${r.code}`}): ${run} (log: ${opts.logPath})`;
|
|
7895
7470
|
}
|
|
7896
7471
|
return;
|
|
7897
7472
|
} finally {
|
|
7898
7473
|
await new Promise((r) => log.end(r));
|
|
7899
7474
|
}
|
|
7900
7475
|
}
|
|
7901
|
-
async function setupOnce(opts) {
|
|
7902
|
-
mkdirSync4(opts.out, { recursive: true });
|
|
7903
|
-
return runAll(opts.commands, opts.root, { ...opts.env, FIREGOAT_OUT: opts.out }, opts.logPath, "setup");
|
|
7904
|
-
}
|
|
7905
|
-
async function prepareRun(opts) {
|
|
7906
|
-
const out = join4(opts.runRoot, "out");
|
|
7907
|
-
if (opts.out === null)
|
|
7908
|
-
mkdirSync4(out);
|
|
7909
|
-
else
|
|
7910
|
-
cpSync(opts.out, out, { recursive: true });
|
|
7911
|
-
const path = [out, opts.env.PATH ?? ""].join(delimiter);
|
|
7912
|
-
const error = await runAll(opts.commands, opts.workdir, {
|
|
7913
|
-
...opts.env,
|
|
7914
|
-
PATH: path,
|
|
7915
|
-
FIREGOAT_OUT: out,
|
|
7916
|
-
FIREGOAT_WORKDIR: opts.workdir
|
|
7917
|
-
}, opts.logPath, "workdir_setup");
|
|
7918
|
-
return error ? { error } : { out, path };
|
|
7919
|
-
}
|
|
7920
7476
|
|
|
7921
7477
|
// src/which.ts
|
|
7922
7478
|
import { accessSync, constants, statSync } from "node:fs";
|
|
7923
|
-
import { delimiter
|
|
7479
|
+
import { delimiter, join as join2, resolve } from "node:path";
|
|
7924
7480
|
function resolveBin(bin) {
|
|
7925
7481
|
const executable = (p) => {
|
|
7926
7482
|
try {
|
|
@@ -7932,39 +7488,15 @@ function resolveBin(bin) {
|
|
|
7932
7488
|
};
|
|
7933
7489
|
if (bin.includes("/"))
|
|
7934
7490
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7935
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7936
|
-
if (dir && executable(
|
|
7937
|
-
return
|
|
7491
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
7492
|
+
if (dir && executable(join2(dir, bin)))
|
|
7493
|
+
return join2(dir, bin);
|
|
7938
7494
|
}
|
|
7939
7495
|
return;
|
|
7940
7496
|
}
|
|
7941
7497
|
|
|
7942
7498
|
// src/eval.ts
|
|
7943
7499
|
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
|
-
}
|
|
7968
7500
|
|
|
7969
7501
|
class UsageError extends Error {
|
|
7970
7502
|
}
|
|
@@ -7972,7 +7504,6 @@ function parseArgs(args) {
|
|
|
7972
7504
|
const opts = {
|
|
7973
7505
|
names: [],
|
|
7974
7506
|
json: false,
|
|
7975
|
-
host: false,
|
|
7976
7507
|
vars: {},
|
|
7977
7508
|
claudeBin: process.env.FIREGOAT_CLAUDE_BIN || "claude"
|
|
7978
7509
|
};
|
|
@@ -7991,8 +7522,6 @@ function parseArgs(args) {
|
|
|
7991
7522
|
};
|
|
7992
7523
|
if (flag === "--json")
|
|
7993
7524
|
opts.json = true;
|
|
7994
|
-
else if (flag === "--host")
|
|
7995
|
-
opts.host = true;
|
|
7996
7525
|
else if (flag === "--var") {
|
|
7997
7526
|
const kv = value();
|
|
7998
7527
|
const at = kv.indexOf("=");
|
|
@@ -8015,151 +7544,135 @@ function parseArgs(args) {
|
|
|
8015
7544
|
}
|
|
8016
7545
|
return opts;
|
|
8017
7546
|
}
|
|
7547
|
+
function findSession(configDir, sessionId) {
|
|
7548
|
+
const projects = join3(configDir, "projects");
|
|
7549
|
+
if (!existsSync3(projects))
|
|
7550
|
+
return null;
|
|
7551
|
+
for (const d of readdirSync(projects)) {
|
|
7552
|
+
const f = join3(projects, d, `${sessionId}.jsonl`);
|
|
7553
|
+
if (existsSync3(f))
|
|
7554
|
+
return f;
|
|
7555
|
+
}
|
|
7556
|
+
return null;
|
|
7557
|
+
}
|
|
8018
7558
|
function newRunId() {
|
|
8019
7559
|
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
8020
7560
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
8021
7561
|
}
|
|
8022
|
-
function gitignoreHasFiregoat(cwd) {
|
|
8023
|
-
const file = join6(cwd, ".gitignore");
|
|
8024
|
-
if (!existsSync4(file))
|
|
8025
|
-
return false;
|
|
8026
|
-
return readFileSync3(file, "utf8").split(`
|
|
8027
|
-
`).map((l) => l.trim()).some((l) => /^\/?\.firegoat\/?(\*\*?)?$/.test(l));
|
|
8028
|
-
}
|
|
8029
|
-
function newRunRoot(label) {
|
|
8030
|
-
const runRoot = realpathSync2(mkdtempSync(join6(tmpdir(), `firegoat-${label}-`)));
|
|
8031
|
-
const workdir = join6(runRoot, "work");
|
|
8032
|
-
mkdirSync5(workdir);
|
|
8033
|
-
return { runRoot, workdir };
|
|
8034
|
-
}
|
|
8035
|
-
function isolate(iso, claudeBin, runRoot, workdir) {
|
|
8036
|
-
if (iso.host)
|
|
8037
|
-
return { profile: null };
|
|
8038
|
-
const profile = sandboxProfile({
|
|
8039
|
-
claudeBin,
|
|
8040
|
-
runRoot,
|
|
8041
|
-
configDir: claudeConfigDir(),
|
|
8042
|
-
repo: iso.repo
|
|
8043
|
-
});
|
|
8044
|
-
const broken = probeSandbox({ profile, workdir, repoFile: iso.repoFile });
|
|
8045
|
-
return broken ? { error: broken } : { profile };
|
|
8046
|
-
}
|
|
8047
7562
|
async function runOne(opts) {
|
|
8048
7563
|
const { test, n } = opts;
|
|
8049
7564
|
const started = Date.now();
|
|
8050
|
-
const dir =
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
const
|
|
7565
|
+
const dir = join3(opts.runDir, test.name, String(n));
|
|
7566
|
+
const workdir = join3(dir, "work");
|
|
7567
|
+
mkdirSync2(workdir, { recursive: true });
|
|
7568
|
+
const r = {
|
|
7569
|
+
test: test.name,
|
|
7570
|
+
n,
|
|
7571
|
+
status: "error",
|
|
7572
|
+
reason: "",
|
|
7573
|
+
prompt: render(test.prompt, opts.vars),
|
|
7574
|
+
end_goal: test.end_goal,
|
|
7575
|
+
checks: [],
|
|
7576
|
+
dir,
|
|
7577
|
+
workdir,
|
|
7578
|
+
session: null,
|
|
7579
|
+
stderr: join3(dir, "stderr.txt"),
|
|
7580
|
+
checksLog: join3(dir, "checks.txt"),
|
|
7581
|
+
num_turns: null,
|
|
7582
|
+
cost: null,
|
|
7583
|
+
duration_ms: 0
|
|
7584
|
+
};
|
|
8054
7585
|
const done = (status, reason) => {
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
7586
|
+
r.status = status;
|
|
7587
|
+
r.reason = reason;
|
|
7588
|
+
r.duration_ms = Date.now() - started;
|
|
7589
|
+
writeFileSync2(join3(dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
7590
|
+
`);
|
|
7591
|
+
return r;
|
|
8060
7592
|
};
|
|
8061
|
-
const {
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
return done("error", `not graded: ${sandbox.error}`);
|
|
8066
|
-
const hostEnv = { ...process.env, ...opts.vars };
|
|
8067
|
-
let extra = {};
|
|
8068
|
-
if (opts.out !== null || opts.workdirSetup.length > 0) {
|
|
8069
|
-
const prepared = await prepareRun({
|
|
8070
|
-
out: opts.out,
|
|
8071
|
-
runRoot,
|
|
8072
|
-
workdir,
|
|
7593
|
+
const stepEnv = { ...opts.stepEnv, FIREGOAT_WORKDIR: workdir };
|
|
7594
|
+
if (opts.workdirSetup.length > 0) {
|
|
7595
|
+
const error = await runSteps({
|
|
7596
|
+
what: "workdir_setup",
|
|
8073
7597
|
commands: opts.workdirSetup,
|
|
8074
|
-
|
|
8075
|
-
|
|
7598
|
+
cwd: workdir,
|
|
7599
|
+
env: stepEnv,
|
|
7600
|
+
logPath: join3(dir, "workdir_setup.txt")
|
|
8076
7601
|
});
|
|
8077
|
-
if (
|
|
8078
|
-
return done("error", `not graded: ${
|
|
8079
|
-
if (opts.out !== null)
|
|
8080
|
-
extra = { PATH: prepared.path, FIREGOAT_OUT: prepared.out };
|
|
7602
|
+
if (error)
|
|
7603
|
+
return done("error", `not graded: ${error}`);
|
|
8081
7604
|
}
|
|
8082
|
-
const
|
|
7605
|
+
const stderr = createWriteStream2(r.stderr);
|
|
8083
7606
|
const outcome = await runClaude({
|
|
8084
|
-
env: {
|
|
8085
|
-
...agentEnv(runRoot, opts.auth.method === "api-key" ? opts.auth.apiKey : undefined),
|
|
8086
|
-
...agentPath
|
|
8087
|
-
},
|
|
8088
|
-
keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
|
|
8089
|
-
profile: sandbox.profile,
|
|
8090
7607
|
bin: opts.claudeBin,
|
|
8091
|
-
prompt:
|
|
7608
|
+
prompt: r.prompt,
|
|
8092
7609
|
cwd: workdir,
|
|
8093
|
-
|
|
8094
|
-
|
|
7610
|
+
env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
|
|
7611
|
+
stderr,
|
|
8095
7612
|
timeoutMs: test.timeout * 1000
|
|
8096
7613
|
});
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
7614
|
+
await new Promise((res) => stderr.end(res));
|
|
7615
|
+
if (outcome.sessionId)
|
|
7616
|
+
r.session = findSession(opts.configDir, outcome.sessionId);
|
|
7617
|
+
r.num_turns = outcome.result?.num_turns ?? null;
|
|
7618
|
+
r.cost = outcome.result?.total_cost_usd ?? null;
|
|
8101
7619
|
if (!outcome.ok)
|
|
8102
7620
|
return done("error", `not graded: ${outcome.reason}`);
|
|
8103
|
-
if (
|
|
8104
|
-
return done("
|
|
8105
|
-
const log =
|
|
8106
|
-
const env = {
|
|
8107
|
-
...process.env,
|
|
8108
|
-
...hostEnv,
|
|
8109
|
-
...extra,
|
|
8110
|
-
FIREGOAT_SESSION: resolve2(session),
|
|
8111
|
-
FIREGOAT_WORKDIR: workdir
|
|
8112
|
-
};
|
|
8113
|
-
const results = [];
|
|
7621
|
+
if (!r.session)
|
|
7622
|
+
return done("error", `not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`);
|
|
7623
|
+
const log = createWriteStream2(r.checksLog);
|
|
8114
7624
|
try {
|
|
8115
|
-
for (const c of test.checks)
|
|
8116
|
-
|
|
7625
|
+
for (const c of test.checks)
|
|
7626
|
+
r.checks.push(await runShell({
|
|
8117
7627
|
run: render(c.run, opts.vars),
|
|
8118
7628
|
cwd: workdir,
|
|
8119
|
-
env,
|
|
7629
|
+
env: { ...stepEnv, FIREGOAT_SESSION: r.session },
|
|
8120
7630
|
timeoutMs: CHECK_TIMEOUT_MS,
|
|
8121
7631
|
log
|
|
8122
7632
|
}));
|
|
8123
|
-
}
|
|
8124
7633
|
} finally {
|
|
8125
|
-
await new Promise((
|
|
7634
|
+
await new Promise((res) => log.end(res));
|
|
8126
7635
|
}
|
|
8127
|
-
|
|
8128
|
-
const broken = results.find((r) => r.notStarted);
|
|
8129
|
-
if (broken)
|
|
8130
|
-
return done("error", `not graded: check ${broken.run}: ${broken.error}`);
|
|
8131
|
-
const failed = results.filter((r) => r.code !== 0);
|
|
7636
|
+
const failed = r.checks.filter((c) => c.code !== 0);
|
|
8132
7637
|
if (failed.length > 0)
|
|
8133
|
-
return done("fail", failed.map((
|
|
8134
|
-
return done("pass", `${
|
|
7638
|
+
return done("fail", failed.map((c) => `check failed (${c.error ?? `exit ${c.code}`}): ${c.run}`).join("; "));
|
|
7639
|
+
return done("pass", test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`);
|
|
8135
7640
|
}
|
|
8136
|
-
async function evalCommand(args,
|
|
7641
|
+
async function evalCommand(args, root) {
|
|
8137
7642
|
if (args.includes("--help") || args.includes("-h")) {
|
|
8138
7643
|
console.log(EVAL_HELP);
|
|
8139
7644
|
return EXIT.pass;
|
|
8140
7645
|
}
|
|
7646
|
+
const json = args.includes("--json");
|
|
7647
|
+
const fatal = (exitCode, error) => {
|
|
7648
|
+
console.error(`firegoat eval: ${error}`);
|
|
7649
|
+
if (json)
|
|
7650
|
+
console.log(JSON.stringify({ error, exitCode }));
|
|
7651
|
+
return exitCode;
|
|
7652
|
+
};
|
|
8141
7653
|
let opts;
|
|
8142
7654
|
try {
|
|
8143
7655
|
opts = parseArgs(args);
|
|
8144
7656
|
} catch (e) {
|
|
8145
|
-
if (e instanceof UsageError)
|
|
8146
|
-
|
|
8147
|
-
return EXIT.usage;
|
|
8148
|
-
}
|
|
7657
|
+
if (e instanceof UsageError)
|
|
7658
|
+
return fatal(EXIT.usage, e.message);
|
|
8149
7659
|
throw e;
|
|
8150
7660
|
}
|
|
8151
7661
|
const say = opts.json ? console.error : console.log;
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
7662
|
+
let config;
|
|
7663
|
+
try {
|
|
7664
|
+
config = loadConfig(root);
|
|
7665
|
+
} catch (e) {
|
|
7666
|
+
if (e instanceof ConfigError)
|
|
7667
|
+
return fatal(EXIT.usage, e.message);
|
|
7668
|
+
throw e;
|
|
7669
|
+
}
|
|
8155
7670
|
let tests = config.tests;
|
|
8156
7671
|
if (opts.names.length > 0) {
|
|
8157
7672
|
const known = new Set(tests.map((t) => t.name));
|
|
8158
7673
|
const unknown = opts.names.filter((n) => !known.has(n));
|
|
8159
|
-
if (unknown.length > 0)
|
|
8160
|
-
|
|
8161
|
-
return EXIT.usage;
|
|
8162
|
-
}
|
|
7674
|
+
if (unknown.length > 0)
|
|
7675
|
+
return fatal(EXIT.usage, `unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
|
|
8163
7676
|
tests = tests.filter((t) => opts.names.includes(t.name));
|
|
8164
7677
|
}
|
|
8165
7678
|
const runs = opts.runs ?? config.runs;
|
|
@@ -8167,572 +7680,205 @@ async function evalCommand(args, cwd) {
|
|
|
8167
7680
|
const missing = [
|
|
8168
7681
|
...new Set(tests.flatMap((t) => [t.prompt, ...t.checks.map((c) => c.run)]).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
8169
7682
|
];
|
|
8170
|
-
if (missing.length > 0)
|
|
8171
|
-
|
|
8172
|
-
return EXIT.usage;
|
|
8173
|
-
}
|
|
7683
|
+
if (missing.length > 0)
|
|
7684
|
+
return fatal(EXIT.usage, `no value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`);
|
|
8174
7685
|
const claudePath = resolveBin(opts.claudeBin);
|
|
8175
|
-
if (!claudePath)
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
else if (process.platform !== "darwin" || !existsSync4(SANDBOX_EXEC)) {
|
|
8182
|
-
console.error(`firegoat eval: isolation needs macOS (${SANDBOX_EXEC}); this is ${process.platform}. In CI or Linux, run inside a throwaway container and pass --host.`);
|
|
8183
|
-
return EXIT.usage;
|
|
8184
|
-
}
|
|
8185
|
-
const iso = {
|
|
8186
|
-
host: opts.host,
|
|
8187
|
-
repo: root,
|
|
8188
|
-
repoFile: join6(root, "firegoat.yaml")
|
|
8189
|
-
};
|
|
8190
|
-
let auth;
|
|
8191
|
-
try {
|
|
8192
|
-
auth = resolveAuth(process.env);
|
|
8193
|
-
} catch (e) {
|
|
8194
|
-
if (e instanceof AuthError) {
|
|
8195
|
-
console.error(`firegoat eval: ${e.message}`);
|
|
8196
|
-
return EXIT.usage;
|
|
8197
|
-
}
|
|
8198
|
-
throw e;
|
|
8199
|
-
}
|
|
8200
|
-
say(`auth: ${auth.label}`);
|
|
8201
|
-
if (auth.method === "claude" && process.env.ANTHROPIC_API_KEY)
|
|
8202
|
-
warn("note: ANTHROPIC_API_KEY in your shell is not passed to the agent (it is used only in CI, or after `firegoat init --auth api-key`)");
|
|
8203
|
-
mkdirSync5(claudeConfigDir(), { recursive: true });
|
|
8204
|
-
const pre = newRunRoot("preflight");
|
|
8205
|
-
const preSandbox = isolate(iso, claudePath, pre.runRoot, pre.workdir);
|
|
8206
|
-
const loginProblem = "error" in preSandbox ? preSandbox.error : auth.method === "claude" ? checkLogin({
|
|
7686
|
+
if (!claudePath)
|
|
7687
|
+
return fatal(EXIT.notGraded, `not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH / FIREGOAT_CLAUDE_BIN.`);
|
|
7688
|
+
const firegoatDir = ensureFiregoatDir(root);
|
|
7689
|
+
const configDir = join3(firegoatDir, ".claude");
|
|
7690
|
+
mkdirSync2(configDir, { recursive: true });
|
|
7691
|
+
const loginProblem = checkLogin({
|
|
8207
7692
|
bin: claudePath,
|
|
8208
|
-
env:
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
console.error(`firegoat eval: not graded: ${loginProblem}`);
|
|
8214
|
-
return EXIT.notGraded;
|
|
8215
|
-
}
|
|
8216
|
-
mkdirSync5(join6(root, ".firegoat"), { recursive: true });
|
|
8217
|
-
if (!gitignoreHasFiregoat(root))
|
|
8218
|
-
warn("hint: add .firegoat/ to .gitignore (it holds run traces)");
|
|
7693
|
+
env: claudeEnv(configDir),
|
|
7694
|
+
cwd: root
|
|
7695
|
+
});
|
|
7696
|
+
if (loginProblem)
|
|
7697
|
+
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
8219
7698
|
const runId = newRunId();
|
|
8220
|
-
const runDir =
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
7699
|
+
const runDir = join3(firegoatDir, "runs", runId);
|
|
7700
|
+
const out = join3(runDir, "out");
|
|
7701
|
+
mkdirSync2(out, { recursive: true });
|
|
7702
|
+
const stepEnv = {
|
|
7703
|
+
...process.env,
|
|
7704
|
+
...vars,
|
|
7705
|
+
FIREGOAT_OUT: out,
|
|
7706
|
+
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
7707
|
+
};
|
|
8224
7708
|
if (config.setup.length > 0) {
|
|
8225
7709
|
say("setup...");
|
|
8226
|
-
|
|
8227
|
-
|
|
7710
|
+
const error = await runSteps({
|
|
7711
|
+
what: "setup",
|
|
8228
7712
|
commands: config.setup,
|
|
8229
|
-
root,
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
logPath: join6(runDir, "setup.txt")
|
|
7713
|
+
cwd: root,
|
|
7714
|
+
env: stepEnv,
|
|
7715
|
+
logPath: join3(runDir, "setup.txt")
|
|
8233
7716
|
});
|
|
8234
|
-
if (error)
|
|
8235
|
-
|
|
8236
|
-
return EXIT.notGraded;
|
|
8237
|
-
}
|
|
7717
|
+
if (error)
|
|
7718
|
+
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
8238
7719
|
}
|
|
8239
7720
|
const results = [];
|
|
8240
|
-
const
|
|
8241
|
-
const totals = { pass: 0, fail: 0, error: 0 };
|
|
8242
|
-
for (const r of results)
|
|
8243
|
-
totals[r.status]++;
|
|
8244
|
-
const doc = {
|
|
8245
|
-
runId,
|
|
8246
|
-
runsDir: runDir,
|
|
8247
|
-
isolation: opts.host ? "host" : "seatbelt",
|
|
8248
|
-
auth: auth.method,
|
|
8249
|
-
grading: GRADING_HINT,
|
|
8250
|
-
totals,
|
|
8251
|
-
runs: results
|
|
8252
|
-
};
|
|
8253
|
-
writeFileSync2(summaryPath, `${JSON.stringify(doc, null, 2)}
|
|
8254
|
-
`);
|
|
8255
|
-
return doc;
|
|
8256
|
-
};
|
|
8257
|
-
for (const test of tests) {
|
|
7721
|
+
for (const test of tests)
|
|
8258
7722
|
for (let n = 1;n <= runs; n++) {
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
workdirSetup: config.workdirSetup,
|
|
8270
|
-
vars,
|
|
8271
|
-
warn
|
|
8272
|
-
});
|
|
8273
|
-
} catch (e) {
|
|
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);
|
|
8279
|
-
}
|
|
7723
|
+
const r = await runOne({
|
|
7724
|
+
test,
|
|
7725
|
+
n,
|
|
7726
|
+
runDir,
|
|
7727
|
+
claudeBin: claudePath,
|
|
7728
|
+
configDir,
|
|
7729
|
+
workdirSetup: config.workdirSetup,
|
|
7730
|
+
stepEnv,
|
|
7731
|
+
vars
|
|
7732
|
+
});
|
|
8280
7733
|
results.push(r);
|
|
8281
|
-
writeSummary();
|
|
8282
7734
|
say(`${r.status.toUpperCase()} ${r.test}#${r.n} ${r.reason}`);
|
|
8283
7735
|
}
|
|
8284
|
-
}
|
|
8285
|
-
const
|
|
8286
|
-
|
|
7736
|
+
const totals = { pass: 0, fail: 0, error: 0 };
|
|
7737
|
+
for (const r of results)
|
|
7738
|
+
totals[r.status]++;
|
|
7739
|
+
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
7740
|
+
writeFileSync2(join3(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
7741
|
+
`);
|
|
8287
7742
|
say(`
|
|
8288
|
-
${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
|
|
8289
|
-
say(`runs: ${runDir}`);
|
|
8290
|
-
say(`end_goal is not graded by firegoat; per run: ${join6(runDir, "<test>", "<n>", "run.json")}`);
|
|
7743
|
+
${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${results.length} runs)`);
|
|
7744
|
+
say(`runs: ${runDir} (end_goal is for you to judge: see run.json per run)`);
|
|
8291
7745
|
if (opts.json)
|
|
8292
7746
|
console.log(JSON.stringify(doc, null, 2));
|
|
8293
|
-
return error > 0 ? EXIT.notGraded : fail > 0 ? EXIT.fail : EXIT.pass;
|
|
7747
|
+
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
8294
7748
|
}
|
|
8295
7749
|
|
|
8296
7750
|
// src/init.ts
|
|
8297
|
-
import { spawnSync as
|
|
8298
|
-
import { existsSync as
|
|
8299
|
-
import { basename
|
|
8300
|
-
import { createInterface
|
|
8301
|
-
import { Writable } from "node:stream";
|
|
8302
|
-
var SKILLS = ["firegoat-onboarding", "suggesting-eval-prompts"];
|
|
7751
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7752
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7753
|
+
import { basename, join as join4 } from "node:path";
|
|
7754
|
+
import { createInterface } from "node:readline/promises";
|
|
8303
7755
|
var SKILLS_ARGV = [
|
|
8304
7756
|
"skills",
|
|
8305
7757
|
"add",
|
|
8306
|
-
"
|
|
8307
|
-
|
|
7758
|
+
"firegoat-ai/skills",
|
|
7759
|
+
"--skill",
|
|
7760
|
+
"firegoat-onboarding",
|
|
7761
|
+
"--skill",
|
|
7762
|
+
"suggesting-eval-prompts",
|
|
8308
7763
|
"--agent",
|
|
8309
7764
|
"claude-code",
|
|
7765
|
+
"--copy",
|
|
8310
7766
|
"-y"
|
|
8311
7767
|
];
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
}
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
else if (pkg.bin && typeof pkg.bin === "object")
|
|
8352
|
-
bin = Object.keys(pkg.bin)[0];
|
|
8353
|
-
const library = !bin || pkg.main !== undefined || pkg.exports !== undefined || pkg.module !== undefined;
|
|
8354
|
-
const docsUrl = typeof pkg.homepage === "string" && /^https?:\/\//.test(pkg.homepage) ? pkg.homepage : undefined;
|
|
8355
|
-
return { ecosystem: "npm", name, bin, library, readme, docsUrl };
|
|
8356
|
-
} catch {}
|
|
8357
|
-
}
|
|
8358
|
-
const pyText = readText(join7(cwd, "pyproject.toml"));
|
|
8359
|
-
if (pyText !== undefined) {
|
|
8360
|
-
const s = tomlSections(pyText);
|
|
8361
|
-
const name = s.get("project")?.get("name") ?? s.get("tool.poetry")?.get("name") ?? fallback.name;
|
|
8362
|
-
const scripts = s.get("project.scripts") ?? s.get("tool.poetry.scripts");
|
|
8363
|
-
const bin = scripts ? [...scripts.keys()][0] : undefined;
|
|
8364
|
-
const urls = s.get("project.urls");
|
|
8365
|
-
const docsUrl = urls?.get("Documentation") ?? urls?.get("documentation") ?? urls?.get("Homepage") ?? urls?.get("homepage");
|
|
8366
|
-
return { ecosystem: "python", name, bin, library: true, readme, docsUrl };
|
|
8367
|
-
}
|
|
8368
|
-
const goText = readText(join7(cwd, "go.mod"));
|
|
8369
|
-
if (goText !== undefined) {
|
|
8370
|
-
const name = goText.match(/^module\s+(\S+)/m)?.[1] ?? fallback.name;
|
|
8371
|
-
return { ecosystem: "go", name, library: true, readme };
|
|
8372
|
-
}
|
|
8373
|
-
const cargoText = readText(join7(cwd, "Cargo.toml"));
|
|
8374
|
-
if (cargoText !== undefined) {
|
|
8375
|
-
const name = tomlSections(cargoText).get("package")?.get("name");
|
|
8376
|
-
return {
|
|
8377
|
-
ecosystem: "rust",
|
|
8378
|
-
name: name ?? fallback.name,
|
|
8379
|
-
library: true,
|
|
8380
|
-
readme
|
|
8381
|
-
};
|
|
8382
|
-
}
|
|
8383
|
-
return fallback;
|
|
8384
|
-
}
|
|
8385
|
-
var LANG = {
|
|
8386
|
-
npm: "Node.js",
|
|
8387
|
-
python: "Python",
|
|
8388
|
-
go: "Go",
|
|
8389
|
-
rust: "Rust"
|
|
8390
|
-
};
|
|
8391
|
-
function importName(p) {
|
|
8392
|
-
if (p.ecosystem === "python" || p.ecosystem === "rust")
|
|
8393
|
-
return p.name.replace(/-/g, "_");
|
|
8394
|
-
return p.name;
|
|
8395
|
-
}
|
|
8396
|
-
var GREP_EXCLUDES = [
|
|
8397
|
-
"--exclude=output.txt",
|
|
8398
|
-
"--exclude=package.json",
|
|
8399
|
-
"--exclude=package-lock.json",
|
|
8400
|
-
"--exclude=pyproject.toml",
|
|
8401
|
-
"--exclude=requirements.txt",
|
|
8402
|
-
"--exclude=go.mod",
|
|
8403
|
-
"--exclude=go.sum",
|
|
8404
|
-
"--exclude=Cargo.toml",
|
|
8405
|
-
"--exclude=Cargo.lock",
|
|
8406
|
-
"--exclude-dir=node_modules",
|
|
8407
|
-
"--exclude-dir=.venv",
|
|
8408
|
-
"--exclude-dir=target"
|
|
8409
|
-
].join(" ");
|
|
8410
|
-
function sq(s) {
|
|
8411
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
8412
|
-
}
|
|
8413
|
-
function templates(p) {
|
|
8414
|
-
const docs = p.docsUrl ? `its docs at ${p.docsUrl}` : "its official docs";
|
|
8415
|
-
const out = [];
|
|
8416
|
-
if (p.bin) {
|
|
8417
|
-
out.push({
|
|
8418
|
-
name: "cli-first-run",
|
|
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.`,
|
|
8420
|
-
end_goal: `cli-output.txt exists and holds real output from running \`${p.bin}\`, not an install log or an error.`,
|
|
8421
|
-
checks: [
|
|
8422
|
-
"test -s cli-output.txt",
|
|
8423
|
-
"! grep -qiE 'command not found|not recognized|ENOENT' cli-output.txt"
|
|
8424
|
-
]
|
|
8425
|
-
});
|
|
8426
|
-
}
|
|
8427
|
-
if (p.library && p.ecosystem) {
|
|
8428
|
-
out.push({
|
|
8429
|
-
name: "quickstart",
|
|
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.`,
|
|
8431
|
-
end_goal: `A program in the workdir uses ${p.name}, runs without errors, and its output is saved in output.txt.`,
|
|
8432
|
-
checks: [
|
|
8433
|
-
"test -s output.txt",
|
|
8434
|
-
`grep -rqsF ${GREP_EXCLUDES} -- ${sq(importName(p))} .`
|
|
8435
|
-
]
|
|
8436
|
-
});
|
|
8437
|
-
}
|
|
8438
|
-
if (out.length === 0) {
|
|
8439
|
-
out.push({
|
|
8440
|
-
name: "quickstart",
|
|
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.`,
|
|
8442
|
-
end_goal: `The ${p.name} quickstart example runs without errors and its output is saved in output.txt.`,
|
|
8443
|
-
checks: ["test -s output.txt"]
|
|
8444
|
-
});
|
|
8445
|
-
}
|
|
8446
|
-
return out;
|
|
8447
|
-
}
|
|
8448
|
-
function block(text, indent) {
|
|
8449
|
-
return text.split(`
|
|
8450
|
-
`).map((l) => indent + l).join(`
|
|
8451
|
-
`);
|
|
8452
|
-
}
|
|
8453
|
-
function renderYaml(p, tests) {
|
|
8454
|
-
const lines = [
|
|
8455
|
-
`# firegoat.yaml: evals for how well coding agents use ${p.name}.`,
|
|
8456
|
-
"# Each test runs a coding agent in an empty workdir with `prompt` as the first message.",
|
|
8457
|
-
"# These are starter templates: edit the prompts to match real user problems.",
|
|
8458
|
-
"# Good prompts state one concrete outcome as a user would, not implementation steps,",
|
|
8459
|
-
"# and ask for observable proof (stdout or a file) that `checks` can verify.",
|
|
8460
|
-
"",
|
|
8461
|
-
"# Agents to evaluate (v1 supports: claude-code).",
|
|
8462
|
-
"agents: [claude-code]",
|
|
8463
|
-
"# How many times to run each test.",
|
|
8464
|
-
"runs: 1",
|
|
8465
|
-
"",
|
|
8466
|
-
"# Optional: test your local build instead of the published package.",
|
|
8467
|
-
"# setup: runs once per eval, in this folder, on your machine. Whatever it writes to",
|
|
8468
|
-
"# $FIREGOAT_OUT is copied into each run's folder and put on the agent's PATH.",
|
|
8469
|
-
"# workdir_setup: runs in each fresh workdir before the agent starts. Then tell the",
|
|
8470
|
-
'# agent in the prompt: "The package is already installed from the build under test."',
|
|
8471
|
-
"#",
|
|
8472
|
-
"# npm:",
|
|
8473
|
-
'# setup: npm pack --pack-destination "$FIREGOAT_OUT"',
|
|
8474
|
-
'# workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz',
|
|
8475
|
-
"#",
|
|
8476
|
-
"# python:",
|
|
8477
|
-
'# setup: uv build --wheel --out-dir "$FIREGOAT_OUT"',
|
|
8478
|
-
'# workdir_setup: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl',
|
|
8479
|
-
"#",
|
|
8480
|
-
"# a CLI binary (lands on PATH as is):",
|
|
8481
|
-
'# setup: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli',
|
|
8482
|
-
"#",
|
|
8483
|
-
"# Values for {{vars.KEY}} in prompts and checks, overridden by `--var KEY=value`;",
|
|
8484
|
-
"# setup commands and checks also get them as env vars.",
|
|
8485
|
-
"# vars:",
|
|
8486
|
-
"# SHA: main",
|
|
8487
|
-
"",
|
|
8488
|
-
"tests:"
|
|
8489
|
-
];
|
|
8490
|
-
for (const t of tests) {
|
|
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");
|
|
8492
|
-
}
|
|
8493
|
-
return `${lines.join(`
|
|
8494
|
-
`)}
|
|
7768
|
+
function template(product) {
|
|
7769
|
+
return `# firegoat.yaml: evals for how well coding agents use ${product}.
|
|
7770
|
+
# Each test runs your claude in an empty folder (.firegoat/runs/...) with \`prompt\`
|
|
7771
|
+
# as the first message, then runs \`checks\` there. Reference: npx firegoat init --help
|
|
7772
|
+
|
|
7773
|
+
# How many times to run each test.
|
|
7774
|
+
runs: 1
|
|
7775
|
+
|
|
7776
|
+
# Optional: test your local build instead of the published package.
|
|
7777
|
+
# setup: runs once per eval, here; what it writes to $FIREGOAT_OUT is put on the
|
|
7778
|
+
# agent's PATH. workdir_setup: runs in each fresh folder before the agent starts.
|
|
7779
|
+
#
|
|
7780
|
+
# npm:
|
|
7781
|
+
# setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7782
|
+
# workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7783
|
+
#
|
|
7784
|
+
# python:
|
|
7785
|
+
# setup: uv build --wheel --out-dir "$FIREGOAT_OUT"
|
|
7786
|
+
# workdir_setup: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl
|
|
7787
|
+
#
|
|
7788
|
+
# a CLI binary:
|
|
7789
|
+
# setup: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli
|
|
7790
|
+
|
|
7791
|
+
tests:
|
|
7792
|
+
- name: quickstart
|
|
7793
|
+
# What a user would ask their agent. Edit this.
|
|
7794
|
+
prompt: >-
|
|
7795
|
+
I want to try ${product}. Get the smallest working example going in a new
|
|
7796
|
+
project, run it, and save what it prints to output.txt. Keep it minimal.
|
|
7797
|
+
# True when the user's problem is solved. Not graded by firegoat: it goes to
|
|
7798
|
+
# your agent (eval --json, run.json) to judge.
|
|
7799
|
+
end_goal: >-
|
|
7800
|
+
A minimal project uses ${product}, runs without errors, and output.txt holds
|
|
7801
|
+
its output.
|
|
7802
|
+
# Shell commands run in the agent's folder afterwards; exit 0 = pass.
|
|
7803
|
+
checks:
|
|
7804
|
+
- run: test -s output.txt
|
|
7805
|
+
# Seconds before the agent run is stopped.
|
|
7806
|
+
timeout: 600
|
|
8495
7807
|
`;
|
|
8496
7808
|
}
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
return
|
|
8508
|
-
const sep = text === "" || text.endsWith(`
|
|
8509
|
-
`) ? "" : `
|
|
8510
|
-
`;
|
|
8511
|
-
writeFileSync3(file, `${text}${sep}${GITIGNORE_LINE}
|
|
8512
|
-
`);
|
|
8513
|
-
return `added ${GITIGNORE_LINE} to .gitignore`;
|
|
8514
|
-
}
|
|
8515
|
-
function installSkills(cwd) {
|
|
8516
|
-
const r = spawnSync4("npx", ["-y", ...SKILLS_ARGV], {
|
|
8517
|
-
cwd,
|
|
8518
|
-
stdio: "inherit"
|
|
8519
|
-
});
|
|
8520
|
-
if (r.error)
|
|
8521
|
-
return r.error.message;
|
|
8522
|
-
if (r.status !== 0)
|
|
8523
|
-
return `exit ${r.status ?? "by signal"}`;
|
|
8524
|
-
return;
|
|
8525
|
-
}
|
|
8526
|
-
async function ask(question) {
|
|
8527
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8528
|
-
try {
|
|
8529
|
-
return (await rl.question(question)).trim();
|
|
8530
|
-
} finally {
|
|
8531
|
-
rl.close();
|
|
8532
|
-
}
|
|
8533
|
-
}
|
|
8534
|
-
async function readSecret(question) {
|
|
8535
|
-
if (!process.stdin.isTTY) {
|
|
8536
|
-
const chunks = [];
|
|
8537
|
-
for await (const c of process.stdin)
|
|
8538
|
-
chunks.push(c);
|
|
8539
|
-
return Buffer.concat(chunks).toString("utf8").trim();
|
|
8540
|
-
}
|
|
8541
|
-
process.stdout.write(question);
|
|
8542
|
-
const silent = new Writable({ write: (_c, _e, cb) => cb() });
|
|
8543
|
-
const rl = createInterface2({
|
|
8544
|
-
input: process.stdin,
|
|
8545
|
-
output: silent,
|
|
8546
|
-
terminal: true
|
|
7809
|
+
var AGENT_PROMPT = "Scan my project and setup 2-3 agent experience prompts to test general usage of the app. Use firegoat skills";
|
|
7810
|
+
var CLIPBOARD = [
|
|
7811
|
+
["pbcopy", []],
|
|
7812
|
+
["wl-copy", []],
|
|
7813
|
+
["xclip", ["-selection", "clipboard"]],
|
|
7814
|
+
["clip", []]
|
|
7815
|
+
];
|
|
7816
|
+
function copyToClipboard(text) {
|
|
7817
|
+
return CLIPBOARD.some(([cmd, args]) => {
|
|
7818
|
+
const r = spawnSync2(cmd, args, { input: text });
|
|
7819
|
+
return !r.error && r.status === 0;
|
|
8547
7820
|
});
|
|
8548
|
-
try {
|
|
8549
|
-
return (await rl.question("")).trim();
|
|
8550
|
-
} finally {
|
|
8551
|
-
rl.close();
|
|
8552
|
-
process.stdout.write(`
|
|
8553
|
-
`);
|
|
8554
|
-
}
|
|
8555
7821
|
}
|
|
8556
|
-
async function
|
|
8557
|
-
|
|
7822
|
+
async function askYes(question) {
|
|
7823
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8558
7824
|
try {
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
current = e.message;
|
|
8562
|
-
}
|
|
8563
|
-
console.log(`How should the agent sign in? (now: ${current})
|
|
8564
|
-
1) Login with Claude (your Claude plan)
|
|
8565
|
-
2) Paste an Anthropic API key`);
|
|
8566
|
-
for (;; ) {
|
|
8567
|
-
const a = await ask("choose [1/2]: ");
|
|
8568
|
-
if (a === "1")
|
|
8569
|
-
return "claude";
|
|
8570
|
-
if (a === "2")
|
|
8571
|
-
return "api-key";
|
|
8572
|
-
console.log("type 1 or 2");
|
|
8573
|
-
}
|
|
8574
|
-
}
|
|
8575
|
-
async function setUpAuth(method) {
|
|
8576
|
-
if (method === "claude") {
|
|
8577
|
-
const code = claudeLoginCommand([]);
|
|
8578
|
-
return code === 0 ? undefined : code;
|
|
8579
|
-
}
|
|
8580
|
-
const key = await readSecret("Anthropic API key (input hidden): ");
|
|
8581
|
-
if (!looksLikeAnthropicKey(key)) {
|
|
8582
|
-
console.error("firegoat init: that does not look like an Anthropic API key (sk-ant-...); nothing stored");
|
|
8583
|
-
return EXIT.usage;
|
|
8584
|
-
}
|
|
8585
|
-
const file = saveApiKey(key);
|
|
8586
|
-
console.log(`stored the API key in ${file} (mode 0600, not in the repo); evals pass it to the agent as ANTHROPIC_API_KEY`);
|
|
8587
|
-
return;
|
|
8588
|
-
}
|
|
8589
|
-
async function askYesNo(question) {
|
|
8590
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8591
|
-
try {
|
|
8592
|
-
const answer = await rl.question(`${question} [y/N] `);
|
|
8593
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
7825
|
+
const a = (await rl.question(`${question} [Y/n] `)).trim();
|
|
7826
|
+
return a === "" || /^y(es)?$/i.test(a);
|
|
8594
7827
|
} finally {
|
|
8595
7828
|
rl.close();
|
|
8596
7829
|
}
|
|
8597
7830
|
}
|
|
8598
|
-
function describe(p) {
|
|
8599
|
-
if (!p.ecosystem)
|
|
8600
|
-
return "no package manifest found, writing a generic test";
|
|
8601
|
-
const parts = [`${p.ecosystem} package ${p.name}`];
|
|
8602
|
-
if (p.bin)
|
|
8603
|
-
parts.push(`bin: ${p.bin}`);
|
|
8604
|
-
if (p.readme)
|
|
8605
|
-
parts.push(`docs: ${p.readme}`);
|
|
8606
|
-
return parts.join(", ");
|
|
8607
|
-
}
|
|
8608
7831
|
async function initCommand(args, cwd) {
|
|
8609
|
-
let yes = false;
|
|
8610
7832
|
let force = false;
|
|
8611
|
-
let skills =
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
}
|
|
8624
|
-
switch (a) {
|
|
8625
|
-
case "--yes":
|
|
8626
|
-
case "-y":
|
|
8627
|
-
yes = true;
|
|
8628
|
-
break;
|
|
8629
|
-
case "--force":
|
|
8630
|
-
case "-f":
|
|
8631
|
-
force = true;
|
|
8632
|
-
break;
|
|
8633
|
-
case "--skills":
|
|
8634
|
-
skills = true;
|
|
8635
|
-
break;
|
|
8636
|
-
case "--help":
|
|
8637
|
-
case "-h":
|
|
8638
|
-
console.log(INIT_HELP);
|
|
8639
|
-
return 0;
|
|
8640
|
-
default:
|
|
8641
|
-
console.error(`firegoat init: unknown argument: ${a}
|
|
7833
|
+
let skills = true;
|
|
7834
|
+
for (const a of args) {
|
|
7835
|
+
if (a === "--help" || a === "-h") {
|
|
7836
|
+
console.log(INIT_HELP);
|
|
7837
|
+
return EXIT.pass;
|
|
7838
|
+
}
|
|
7839
|
+
if (a === "--force" || a === "-f")
|
|
7840
|
+
force = true;
|
|
7841
|
+
else if (a === "--no-skills")
|
|
7842
|
+
skills = false;
|
|
7843
|
+
else {
|
|
7844
|
+
console.error(`firegoat init: unknown argument: ${a}
|
|
8642
7845
|
|
|
8643
7846
|
${INIT_HELP}`);
|
|
8644
|
-
|
|
8645
|
-
}
|
|
8646
|
-
}
|
|
8647
|
-
|
|
8648
|
-
const
|
|
8649
|
-
if (
|
|
8650
|
-
console.
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
console.log(`detected: ${describe(project)}`);
|
|
8659
|
-
writeFileSync3(configPath, renderYaml(project, tests));
|
|
8660
|
-
console.log(`wrote ${CONFIG_FILE} (${tests.length} test${tests.length === 1 ? "" : "s"}: ${tests.map((t) => t.name).join(", ")}); edit the prompts to fit your users`);
|
|
8661
|
-
}
|
|
8662
|
-
console.log(ensureGitignore(cwd));
|
|
8663
|
-
const interactive = !yes && process.stdin.isTTY && process.stdout.isTTY;
|
|
8664
|
-
if (!auth && interactive)
|
|
8665
|
-
auth = await chooseAuth();
|
|
8666
|
-
if (auth) {
|
|
8667
|
-
const failed = await setUpAuth(auth);
|
|
8668
|
-
if (failed !== undefined)
|
|
8669
|
-
return failed;
|
|
8670
|
-
}
|
|
8671
|
-
if (!skills && interactive)
|
|
8672
|
-
skills = await askYesNo(`Add the ${SKILLS.join(" and ")} skills to .claude/skills/ (help your agent draft better evals)?`);
|
|
7847
|
+
return EXIT.usage;
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
ensureFiregoatDir(cwd);
|
|
7851
|
+
const configPath = join4(cwd, CONFIG_FILE);
|
|
7852
|
+
if (existsSync4(configPath) && !force)
|
|
7853
|
+
console.log(`kept ${CONFIG_FILE} (--force replaces it)`);
|
|
7854
|
+
else {
|
|
7855
|
+
writeFileSync3(configPath, template(basename(cwd)));
|
|
7856
|
+
console.log(`wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
7857
|
+
}
|
|
7858
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
7859
|
+
if (skills && interactive)
|
|
7860
|
+
skills = await askYes("Install the firegoat-onboarding skill for your coding agent?");
|
|
8673
7861
|
if (skills) {
|
|
8674
7862
|
console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8675
|
-
const
|
|
8676
|
-
|
|
8677
|
-
|
|
7863
|
+
const env = { ...process.env };
|
|
7864
|
+
delete env.npm_config_package;
|
|
7865
|
+
const r = spawnSync2("npx", ["-y", ...SKILLS_ARGV], {
|
|
7866
|
+
cwd,
|
|
7867
|
+
env,
|
|
7868
|
+
stdio: "inherit"
|
|
7869
|
+
});
|
|
7870
|
+
if (r.error || r.status !== 0) {
|
|
7871
|
+
console.error(`firegoat init: installing the skills failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8678
7872
|
return EXIT.notGraded;
|
|
8679
7873
|
}
|
|
8680
|
-
} else {
|
|
8681
|
-
console.log(`hint: \`npx firegoat init --skills\` adds the ${SKILLS.join(" and ")} skills to .claude/skills/ (keeps your ${CONFIG_FILE})`);
|
|
8682
7874
|
}
|
|
8683
|
-
console.log(
|
|
8684
|
-
|
|
8685
|
-
}
|
|
7875
|
+
console.log(`
|
|
7876
|
+
Paste this into your coding agent:
|
|
8686
7877
|
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
var OPENER = {
|
|
8692
|
-
darwin: "open",
|
|
8693
|
-
linux: "xdg-open"
|
|
8694
|
-
};
|
|
8695
|
-
function openCommand(args, cwd) {
|
|
8696
|
-
let print = false;
|
|
8697
|
-
const ids = [];
|
|
8698
|
-
for (const a of args) {
|
|
8699
|
-
if (a === "--help" || a === "-h") {
|
|
8700
|
-
console.log(OPEN_HELP);
|
|
8701
|
-
return EXIT.pass;
|
|
8702
|
-
}
|
|
8703
|
-
if (a === "--print")
|
|
8704
|
-
print = true;
|
|
8705
|
-
else if (a.startsWith("-")) {
|
|
8706
|
-
console.error(`firegoat open: unknown flag: ${a}`);
|
|
8707
|
-
return EXIT.usage;
|
|
8708
|
-
} else
|
|
8709
|
-
ids.push(a);
|
|
8710
|
-
}
|
|
8711
|
-
if (ids.length > 1) {
|
|
8712
|
-
console.error("firegoat open: takes at most one run id");
|
|
8713
|
-
return EXIT.usage;
|
|
8714
|
-
}
|
|
8715
|
-
const base = join8(findConfigDir(cwd), ".firegoat", "runs");
|
|
8716
|
-
const runs = existsSync6(base) ? readdirSync(base).sort() : [];
|
|
8717
|
-
const id = ids[0] ?? runs.at(-1);
|
|
8718
|
-
if (!id) {
|
|
8719
|
-
console.error(`firegoat open: no runs in ${base}. Run: npx firegoat eval`);
|
|
8720
|
-
return EXIT.usage;
|
|
8721
|
-
}
|
|
8722
|
-
if (!runs.includes(id)) {
|
|
8723
|
-
console.error(`firegoat open: no run ${JSON.stringify(id)} in ${base} (latest: ${runs.at(-1) ?? "none"})`);
|
|
8724
|
-
return EXIT.usage;
|
|
8725
|
-
}
|
|
8726
|
-
const dir = join8(base, id);
|
|
8727
|
-
console.log(dir);
|
|
8728
|
-
const opener = OPENER[process.platform];
|
|
8729
|
-
if (print || !opener)
|
|
8730
|
-
return EXIT.pass;
|
|
8731
|
-
const r = spawnSync5(opener, [dir], { stdio: "inherit" });
|
|
8732
|
-
if (r.error || r.status !== 0) {
|
|
8733
|
-
console.error(`firegoat open: ${opener} failed (${r.error?.message ?? `exit ${r.status}`}); the run is at the path above`);
|
|
8734
|
-
return EXIT.notGraded;
|
|
8735
|
-
}
|
|
7878
|
+
${AGENT_PROMPT}
|
|
7879
|
+
`);
|
|
7880
|
+
if (interactive && copyToClipboard(AGENT_PROMPT))
|
|
7881
|
+
console.log("\x1B[32mPrompt copied!\x1B[0m");
|
|
8736
7882
|
return EXIT.pass;
|
|
8737
7883
|
}
|
|
8738
7884
|
|
|
@@ -8751,24 +7897,8 @@ async function main(argv) {
|
|
|
8751
7897
|
return 0;
|
|
8752
7898
|
case "init":
|
|
8753
7899
|
return initCommand(rest, process.cwd());
|
|
8754
|
-
case "claude":
|
|
8755
|
-
if (rest[0] === "login")
|
|
8756
|
-
return claudeLoginCommand(rest.slice(1));
|
|
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}`);
|
|
8764
|
-
return EXIT.usage;
|
|
8765
|
-
case "login":
|
|
8766
|
-
console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
|
|
8767
|
-
return claudeLoginCommand(rest);
|
|
8768
7900
|
case "eval":
|
|
8769
7901
|
return evalCommand(rest, process.cwd());
|
|
8770
|
-
case "open":
|
|
8771
|
-
return openCommand(rest, process.cwd());
|
|
8772
7902
|
default:
|
|
8773
7903
|
console.error(`unknown command: ${cmd}
|
|
8774
7904
|
|