firegoat 0.7.0 → 0.9.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 +72 -50
- package/dist/cli.js +472 -1348
- 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.0",
|
|
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,411 +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, a goal, checks
|
|
7225
|
-
3. ask the human to run once: npx firegoat claude login (opens a browser)
|
|
7226
|
-
4. npx firegoat eval --json run the tests; JSON summary on stdout
|
|
7227
|
-
5. judge each run's goal + rubric yourself, from its run.json, transcript and
|
|
7228
|
-
workdir: firegoat only runs the script checks
|
|
7229
|
-
|
|
7230
|
-
Commands:
|
|
7231
|
-
init write firegoat.yaml; choose how the agent signs in
|
|
7232
|
-
claude login sign claude in to firegoat's own config dir (interactive, once)
|
|
7233
|
-
eval run the tests
|
|
7234
|
-
open open (or --print) a run's folder
|
|
7235
|
-
|
|
7236
|
-
Run firegoat <command> --help for details and examples.
|
|
7237
|
-
|
|
7238
|
-
${EXIT_CODES}`;
|
|
7239
|
-
}
|
|
7240
|
-
var INIT_HELP = `Usage: firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
|
|
7241
|
-
|
|
7242
|
-
Writes firegoat.yaml in the current folder with starter tests from what the repo
|
|
7243
|
-
ships (package.json, pyproject.toml, go.mod, Cargo.toml, README), and adds
|
|
7244
|
-
.firegoat/ to .gitignore. Edit the tests afterwards; they are templates.
|
|
7245
|
-
|
|
7246
|
-
--yes, -y don't ask questions (default when stdin is not a terminal)
|
|
7247
|
-
--force, -f overwrite an existing firegoat.yaml
|
|
7248
|
-
--skills also run: npx skills add team2027/skills --skill firegoat-onboarding
|
|
7249
|
-
--skill suggesting-eval-prompts (into .claude/skills/)
|
|
7250
|
-
--auth claude the agent signs in with a Claude login: runs \`firegoat claude login\`
|
|
7251
|
-
(interactive: a human must complete it in the browser)
|
|
7252
|
-
--auth api-key the agent signs in with an Anthropic API key, read from stdin when
|
|
7253
|
-
piped (else asked for without echo) and stored in
|
|
7254
|
-
~/.firegoat/anthropic-api-key (mode 0600), never in the repo
|
|
7255
|
-
Without --auth, init asks (unless --yes). In CI, ANTHROPIC_API_KEY in
|
|
7256
|
-
the env wins.
|
|
7257
|
-
|
|
7258
|
-
firegoat.yaml reference:
|
|
7259
|
-
agents: [claude-code] only claude-code for now
|
|
7260
|
-
runs: 1 runs per test
|
|
7261
|
-
setup: <sh> | [<sh>, ...] once per eval, in the repo, on the host; whatever it
|
|
7262
|
-
writes to $FIREGOAT_OUT is copied into each run and put
|
|
7263
|
-
first on the agent's PATH
|
|
7264
|
-
workdir_setup: <sh> | [...] in each fresh workdir before the agent starts
|
|
7265
|
-
(env: $FIREGOAT_OUT, $FIREGOAT_WORKDIR, vars)
|
|
7266
|
-
vars: {KEY: value} defaults for {{vars.KEY}}; --var KEY=value overrides
|
|
7267
|
-
tests:
|
|
7268
|
-
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
7269
|
-
prompt: ... what a user would ask the agent; one concrete outcome
|
|
7270
|
-
goal: ... one sentence that is true when the problem is solved
|
|
7271
|
-
rubric: [...] optional criteria for whoever grades the run (you)
|
|
7272
|
-
checks: optional shell commands, run in the workdir after the
|
|
7273
|
-
- run: ... agent; exit 0 = pass; they set eval's exit code
|
|
7274
|
-
timeout: 600 optional, seconds
|
|
7275
|
-
|
|
7276
|
-
The agent's folder has no repo files. Checks get $FIREGOAT_WORKDIR,
|
|
7277
|
-
$FIREGOAT_SESSION (stream-json transcript), $FIREGOAT_OUT and the vars.
|
|
7278
|
-
goal and rubric are not run by firegoat: they are handed to the caller to judge.
|
|
7279
|
-
|
|
7280
|
-
Example:
|
|
7281
|
-
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7282
|
-
workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7283
|
-
tests:
|
|
7284
|
-
- name: first-call
|
|
7285
|
-
prompt: >-
|
|
7286
|
-
Using the acme SDK (already installed from the build under test), send a test
|
|
7287
|
-
event and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
|
|
7288
|
-
goal: A script sends one event and prints its id.
|
|
7289
|
-
rubric:
|
|
7290
|
-
- uses the SDK's client, not raw HTTP
|
|
7291
|
-
- does not install acme from the registry
|
|
7292
|
-
checks:
|
|
7293
|
-
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_SESSION"
|
|
7294
|
-
|
|
7295
|
-
Next: npx firegoat claude login (a human, once), then npx firegoat eval.
|
|
7296
|
-
|
|
7297
|
-
Exit codes: 0 done, 2 the skills install or claude login failed, 3 usage error.`;
|
|
7298
|
-
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host]
|
|
7299
|
-
[--claude-bin PATH]
|
|
7300
|
-
|
|
7301
|
-
Runs each test headless: claude gets the prompt in a fresh folder outside the repo,
|
|
7302
|
-
sandboxed on macOS (it can't read your home dir or the repo), with setup:'s outputs
|
|
7303
|
-
on PATH. Then the test's script checks run in that folder.
|
|
7304
|
-
|
|
7305
|
-
test.. only these tests (by name)
|
|
7306
|
-
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7307
|
-
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7308
|
-
--json the summary as JSON on stdout; progress goes to stderr
|
|
7309
|
-
--host no sandbox (required off macOS; use a throwaway container)
|
|
7310
|
-
--claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
|
|
7311
|
-
|
|
7312
|
-
Sign-in: the one chosen in firegoat init (Claude login or stored API key); in CI,
|
|
7313
|
-
ANTHROPIC_API_KEY from the env. eval prints which. Not logged in = exit 2 with the
|
|
7314
|
-
command to run.
|
|
7315
|
-
|
|
7316
|
-
Output: .firegoat/runs/<runId>/
|
|
7317
|
-
summary.json every run, plus totals and a grading hint
|
|
7318
|
-
<test>/<n>/run.json one run: test, n, status and reason (from the checks),
|
|
7319
|
-
prompt, goal, rubric, checks [{run, code}], and paths:
|
|
7320
|
-
dir, workdir, session, stderr, checksLog
|
|
7321
|
-
<test>/<n>/session.jsonl the agent's transcript, stream-json (one event per line)
|
|
7322
|
-
setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
|
|
7323
|
-
|
|
7324
|
-
Grading: firegoat runs only the script checks; they set the exit code. The goal and
|
|
7325
|
-
rubric are yours to judge, with your own process. session.jsonl is stream-json: the
|
|
7326
|
-
last line has type "result" with the agent's final message; tool calls are
|
|
7327
|
-
"tool_use" items in "assistant" messages; tool output is in "user" messages.
|
|
7328
|
-
|
|
7329
|
-
Examples:
|
|
7330
|
-
npx firegoat eval --json all tests, JSON summary on stdout
|
|
7331
|
-
npx firegoat eval quickstart --runs 3 one test, three runs
|
|
7332
|
-
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
7333
|
-
|
|
7334
|
-
firegoat.yaml is looked up in the current folder, then parents up to the git root.
|
|
7335
|
-
A run can take minutes (default timeout 600s per test).
|
|
7336
|
-
|
|
7337
|
-
${EXIT_CODES}`;
|
|
7338
|
-
var OPEN_HELP = `Usage: firegoat open [runId] [--print]
|
|
7339
|
-
|
|
7340
|
-
Opens a run's folder (.firegoat/runs/<runId>/) with the system opener; the latest
|
|
7341
|
-
run by default. --print only prints the path (use this from an agent).
|
|
7342
|
-
|
|
7343
|
-
Exit codes: 0 done, 2 the opener failed, 3 no such run.`;
|
|
7344
|
-
var CLAUDE_LOGIN_HELP = `Usage: firegoat claude login [--claude-bin PATH]
|
|
7345
|
-
|
|
7346
|
-
Signs claude in to firegoat's own config dir (~/.firegoat/claude), once per machine.
|
|
7347
|
-
Evals then run on that login, never your ~/.claude. Interactive: it opens a browser,
|
|
7348
|
-
so an agent should ask the human to run it. \`firegoat login\` is a deprecated alias.
|
|
7349
|
-
|
|
7350
|
-
Exit codes: 0 logged in, 2 claude missing or the login failed, 3 usage error.`;
|
|
7351
|
-
|
|
7352
|
-
// src/claude-login.ts
|
|
7353
|
-
function claudeLoginCommand(args) {
|
|
7354
|
-
let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
|
|
7355
|
-
for (let i = 0;i < args.length; i++) {
|
|
7356
|
-
const a = args[i];
|
|
7357
|
-
if (a === "--help" || a === "-h") {
|
|
7358
|
-
console.log(CLAUDE_LOGIN_HELP);
|
|
7359
|
-
return EXIT.pass;
|
|
7360
|
-
}
|
|
7361
|
-
if (a === "--claude-bin" && args[i + 1] !== undefined)
|
|
7362
|
-
claudeBin = args[++i];
|
|
7363
|
-
else if (a.startsWith("--claude-bin="))
|
|
7364
|
-
claudeBin = a.slice(13);
|
|
7365
|
-
else {
|
|
7366
|
-
console.error(`firegoat claude login: unknown argument: ${a}`);
|
|
7367
|
-
return EXIT.usage;
|
|
7368
|
-
}
|
|
7369
|
-
}
|
|
7370
|
-
const configDir = claudeConfigDir();
|
|
7371
|
-
mkdirSync3(configDir, { recursive: true });
|
|
7372
|
-
const env = {
|
|
7373
|
-
...process.env,
|
|
7374
|
-
CLAUDE_CONFIG_DIR: configDir
|
|
7375
|
-
};
|
|
7376
|
-
delete env.ANTHROPIC_API_KEY;
|
|
7377
|
-
console.error(`Signing claude in to ${configDir}. Evals run with this login, never your own ~/.claude.`);
|
|
7378
|
-
const r = spawnSync2(claudeBin, ["auth", "login"], { env, stdio: "inherit" });
|
|
7379
|
-
if (r.error) {
|
|
7380
|
-
console.error(`firegoat claude login: could not run ${claudeBin}: ${r.error.message}`);
|
|
7381
|
-
return EXIT.notGraded;
|
|
7382
|
-
}
|
|
7383
|
-
if (r.status !== 0) {
|
|
7384
|
-
console.error(`firegoat claude login: claude auth login exited ${r.status}`);
|
|
7385
|
-
return EXIT.notGraded;
|
|
7386
|
-
}
|
|
7387
|
-
saveMethod("claude");
|
|
7388
|
-
console.log("logged in; evals now sign in with this Claude login. next: npx firegoat eval");
|
|
7389
|
-
return EXIT.pass;
|
|
7390
|
-
}
|
|
7391
|
-
|
|
7392
6988
|
// src/config.ts
|
|
7393
|
-
import { existsSync
|
|
7394
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
6989
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7395
6990
|
|
|
7396
6991
|
// node_modules/yaml/dist/index.js
|
|
7397
6992
|
var composer = require_composer();
|
|
@@ -7442,7 +7037,6 @@ var $visitAsync = visit.visitAsync;
|
|
|
7442
7037
|
// src/config.ts
|
|
7443
7038
|
var CONFIG_FILE = "firegoat.yaml";
|
|
7444
7039
|
var DEFAULT_TIMEOUT = 600;
|
|
7445
|
-
var SUPPORTED_AGENTS = ["claude-code"];
|
|
7446
7040
|
|
|
7447
7041
|
class ConfigError extends Error {
|
|
7448
7042
|
}
|
|
@@ -7464,6 +7058,11 @@ function posInt(v, path, fallback) {
|
|
|
7464
7058
|
fail(path, "must be a positive integer");
|
|
7465
7059
|
return v;
|
|
7466
7060
|
}
|
|
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
|
+
}
|
|
7467
7066
|
function parseConfig(text) {
|
|
7468
7067
|
let raw;
|
|
7469
7068
|
try {
|
|
@@ -7473,17 +7072,8 @@ function parseConfig(text) {
|
|
|
7473
7072
|
}
|
|
7474
7073
|
if (!isRecord(raw))
|
|
7475
7074
|
fail("(root)", "must be a mapping");
|
|
7476
|
-
|
|
7477
|
-
if (!Array.isArray(agents) || agents.length === 0)
|
|
7478
|
-
fail("agents", "must be a non-empty list");
|
|
7479
|
-
agents.forEach((a, i) => {
|
|
7480
|
-
if (!SUPPORTED_AGENTS.includes(a)) {
|
|
7481
|
-
fail(`agents[${i}]`, `unsupported agent ${JSON.stringify(a)} (v1 supports: ${SUPPORTED_AGENTS.join(", ")})`);
|
|
7482
|
-
}
|
|
7483
|
-
});
|
|
7075
|
+
onlyKeys(raw, "(root)", ["runs", "setup", "workdir_setup", "vars", "tests"]);
|
|
7484
7076
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7485
|
-
if (raw.build !== undefined)
|
|
7486
|
-
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");
|
|
7487
7077
|
const setup = commands(raw.setup, "setup");
|
|
7488
7078
|
const workdirSetup = commands(raw.workdir_setup, "workdir_setup");
|
|
7489
7079
|
const vars = parseVars(raw.vars);
|
|
@@ -7494,6 +7084,7 @@ function parseConfig(text) {
|
|
|
7494
7084
|
const p = `tests[${i}]`;
|
|
7495
7085
|
if (!isRecord(t))
|
|
7496
7086
|
fail(p, "must be a mapping");
|
|
7087
|
+
onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout"]);
|
|
7497
7088
|
const name = str(t.name, `${p}.name`);
|
|
7498
7089
|
if (!NAME.test(name))
|
|
7499
7090
|
fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
|
|
@@ -7504,27 +7095,22 @@ function parseConfig(text) {
|
|
|
7504
7095
|
if (!Array.isArray(checksRaw))
|
|
7505
7096
|
fail(`${p}.checks`, "must be a list");
|
|
7506
7097
|
const checks = [];
|
|
7507
|
-
const rubric = rubricOf(t.rubric, `${p}.rubric`);
|
|
7508
7098
|
checksRaw.forEach((c, j) => {
|
|
7509
7099
|
const cp = `${p}.checks[${j}]`;
|
|
7510
7100
|
if (!isRecord(c))
|
|
7511
7101
|
fail(cp, "must be a mapping");
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
else
|
|
7515
|
-
checks.push({ run: str(c.run, `${cp}.run`) });
|
|
7102
|
+
onlyKeys(c, cp, ["run"]);
|
|
7103
|
+
checks.push({ run: str(c.run, `${cp}.run`) });
|
|
7516
7104
|
});
|
|
7517
7105
|
return {
|
|
7518
7106
|
name,
|
|
7519
7107
|
prompt: str(t.prompt, `${p}.prompt`),
|
|
7520
|
-
|
|
7521
|
-
rubric,
|
|
7108
|
+
end_goal: str(t.end_goal, `${p}.end_goal`),
|
|
7522
7109
|
checks,
|
|
7523
7110
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
|
|
7524
7111
|
};
|
|
7525
7112
|
});
|
|
7526
7113
|
return {
|
|
7527
|
-
agents,
|
|
7528
7114
|
runs,
|
|
7529
7115
|
setup,
|
|
7530
7116
|
workdirSetup,
|
|
@@ -7533,15 +7119,6 @@ function parseConfig(text) {
|
|
|
7533
7119
|
};
|
|
7534
7120
|
}
|
|
7535
7121
|
var NAME = /^[A-Za-z0-9._-]+$/;
|
|
7536
|
-
function rubricOf(raw, path) {
|
|
7537
|
-
if (raw === undefined)
|
|
7538
|
-
return [];
|
|
7539
|
-
if (typeof raw === "string")
|
|
7540
|
-
return [str(raw, path)];
|
|
7541
|
-
if (!Array.isArray(raw))
|
|
7542
|
-
fail(path, "must be a string or a list of strings");
|
|
7543
|
-
return raw.map((r, i) => str(r, `${path}[${i}]`));
|
|
7544
|
-
}
|
|
7545
7122
|
function commands(raw, path) {
|
|
7546
7123
|
if (raw === undefined)
|
|
7547
7124
|
return [];
|
|
@@ -7581,258 +7158,132 @@ function render(text, vars) {
|
|
|
7581
7158
|
}
|
|
7582
7159
|
function loadConfig(dir) {
|
|
7583
7160
|
const file = `${dir}/${CONFIG_FILE}`;
|
|
7584
|
-
if (!
|
|
7161
|
+
if (!existsSync(file))
|
|
7585
7162
|
throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
|
|
7586
|
-
return parseConfig(
|
|
7587
|
-
}
|
|
7588
|
-
function findConfigDir(cwd) {
|
|
7589
|
-
let dir = cwd;
|
|
7590
|
-
for (;; ) {
|
|
7591
|
-
if (existsSync3(join3(dir, CONFIG_FILE)))
|
|
7592
|
-
return dir;
|
|
7593
|
-
const parent = dirname2(dir);
|
|
7594
|
-
if (existsSync3(join3(dir, ".git")) || parent === dir)
|
|
7595
|
-
return cwd;
|
|
7596
|
-
dir = parent;
|
|
7597
|
-
}
|
|
7163
|
+
return parseConfig(readFileSync(file, "utf8"));
|
|
7598
7164
|
}
|
|
7599
7165
|
|
|
7600
7166
|
// src/eval.ts
|
|
7601
7167
|
import { randomBytes } from "node:crypto";
|
|
7602
7168
|
import {
|
|
7603
|
-
createWriteStream as
|
|
7604
|
-
existsSync as
|
|
7605
|
-
mkdirSync as
|
|
7606
|
-
|
|
7607
|
-
readFileSync as readFileSync3,
|
|
7608
|
-
realpathSync as realpathSync2,
|
|
7169
|
+
createWriteStream as createWriteStream2,
|
|
7170
|
+
existsSync as existsSync3,
|
|
7171
|
+
mkdirSync as mkdirSync2,
|
|
7172
|
+
readdirSync,
|
|
7609
7173
|
writeFileSync as writeFileSync2
|
|
7610
7174
|
} from "node:fs";
|
|
7611
|
-
import {
|
|
7612
|
-
import { join as join6, resolve as resolve2 } from "node:path";
|
|
7175
|
+
import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
7613
7176
|
|
|
7614
7177
|
// src/claude.ts
|
|
7615
|
-
import { spawn, spawnSync
|
|
7616
|
-
import { createWriteStream } from "node:fs";
|
|
7617
|
-
import { createInterface } from "node:readline";
|
|
7178
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7618
7179
|
var CLAUDE_FLAGS = [
|
|
7619
7180
|
"--output-format",
|
|
7620
7181
|
"stream-json",
|
|
7621
7182
|
"--verbose",
|
|
7622
7183
|
"--dangerously-skip-permissions",
|
|
7623
|
-
"--no-session-persistence",
|
|
7624
7184
|
"--strict-mcp-config",
|
|
7625
7185
|
"--setting-sources",
|
|
7626
|
-
""
|
|
7186
|
+
"",
|
|
7187
|
+
"--disable-slash-commands"
|
|
7627
7188
|
];
|
|
7628
|
-
function
|
|
7629
|
-
return
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
return;
|
|
7635
|
-
try {
|
|
7636
|
-
process.kill(-child.pid, signal);
|
|
7637
|
-
} catch {
|
|
7638
|
-
try {
|
|
7639
|
-
child.kill(signal);
|
|
7640
|
-
} catch {}
|
|
7641
|
-
}
|
|
7642
|
-
}
|
|
7643
|
-
var signalsHooked = false;
|
|
7644
|
-
function hookSignals() {
|
|
7645
|
-
if (signalsHooked)
|
|
7646
|
-
return;
|
|
7647
|
-
signalsHooked = true;
|
|
7648
|
-
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
7649
|
-
process.on(sig, () => {
|
|
7650
|
-
for (const c of live)
|
|
7651
|
-
killGroup(c, "SIGKILL");
|
|
7652
|
-
process.exit(sig === "SIGINT" ? 130 : 143);
|
|
7653
|
-
});
|
|
7654
|
-
}
|
|
7189
|
+
function claudeEnv(configDir) {
|
|
7190
|
+
return {
|
|
7191
|
+
...process.env,
|
|
7192
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
7193
|
+
CLAUDE_SECURESTORAGE_CONFIG_DIR: ""
|
|
7194
|
+
};
|
|
7655
7195
|
}
|
|
7656
|
-
function
|
|
7657
|
-
hookSignals();
|
|
7196
|
+
function run(opts) {
|
|
7658
7197
|
return new Promise((resolve) => {
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
7665
|
-
detached: true,
|
|
7666
|
-
shell: false
|
|
7667
|
-
});
|
|
7668
|
-
} catch (e) {
|
|
7669
|
-
resolve({
|
|
7670
|
-
code: null,
|
|
7671
|
-
timedOut: false,
|
|
7672
|
-
spawnError: e
|
|
7673
|
-
});
|
|
7674
|
-
return;
|
|
7675
|
-
}
|
|
7676
|
-
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
|
+
});
|
|
7677
7203
|
let timedOut = false;
|
|
7678
|
-
let
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
const finish = (r) => {
|
|
7686
|
-
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)
|
|
7687
7211
|
return;
|
|
7688
|
-
|
|
7212
|
+
settled = true;
|
|
7689
7213
|
clearTimeout(timer);
|
|
7690
|
-
clearTimeout(killTimer);
|
|
7691
|
-
clearTimeout(graceTimer);
|
|
7692
|
-
live.delete(child);
|
|
7693
7214
|
resolve(r);
|
|
7694
7215
|
};
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
};
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
};
|
|
7704
|
-
const timer = setTimeout(() => {
|
|
7705
|
-
timedOut = true;
|
|
7706
|
-
stop();
|
|
7707
|
-
}, opts.timeoutMs);
|
|
7708
|
-
child.on("error", (err) => {
|
|
7709
|
-
if (child.pid === undefined)
|
|
7710
|
-
finish({ code: null, timedOut: false, spawnError: err });
|
|
7711
|
-
});
|
|
7712
|
-
child.on("exit", (code) => {
|
|
7713
|
-
exited = true;
|
|
7714
|
-
exitCode = code;
|
|
7715
|
-
graceTimer = setTimeout(() => {
|
|
7716
|
-
child.stdout?.destroy();
|
|
7717
|
-
child.stderr?.destroy();
|
|
7718
|
-
finish({ code: exitCode, timedOut });
|
|
7719
|
-
}, 2000);
|
|
7720
|
-
maybeFinish();
|
|
7721
|
-
});
|
|
7722
|
-
child.stderr?.on("data", opts.onStderr);
|
|
7723
|
-
child.stderr?.on("close", () => {
|
|
7724
|
-
stderrDone = true;
|
|
7725
|
-
maybeFinish();
|
|
7726
|
-
});
|
|
7727
|
-
if (opts.onLine) {
|
|
7728
|
-
const onLine = opts.onLine;
|
|
7729
|
-
const rl = createInterface({
|
|
7730
|
-
input: child.stdout
|
|
7731
|
-
});
|
|
7732
|
-
rl.on("line", (line) => {
|
|
7733
|
-
if (onLine(line) === "kill")
|
|
7734
|
-
stop();
|
|
7735
|
-
});
|
|
7736
|
-
rl.on("close", () => {
|
|
7737
|
-
linesDone = true;
|
|
7738
|
-
maybeFinish();
|
|
7739
|
-
});
|
|
7740
|
-
} else {
|
|
7741
|
-
if (opts.onStdout)
|
|
7742
|
-
child.stdout?.on("data", opts.onStdout);
|
|
7743
|
-
child.stdout?.on("close", () => {
|
|
7744
|
-
stdoutDone = true;
|
|
7745
|
-
maybeFinish();
|
|
7746
|
-
});
|
|
7747
|
-
}
|
|
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());
|
|
7748
7225
|
});
|
|
7749
7226
|
}
|
|
7750
|
-
function closeStream(s) {
|
|
7751
|
-
return new Promise((r) => s.end(r));
|
|
7752
|
-
}
|
|
7753
7227
|
async function runClaude(opts) {
|
|
7754
|
-
|
|
7755
|
-
const
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
let result;
|
|
7759
|
-
const spawned = await spawnWatched({
|
|
7760
|
-
...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],
|
|
7761
7232
|
cwd: opts.cwd,
|
|
7762
7233
|
env: opts.env,
|
|
7763
7234
|
timeoutMs: opts.timeoutMs,
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
if (line.trim() === "")
|
|
7769
|
-
return;
|
|
7770
|
-
let msg;
|
|
7771
|
-
try {
|
|
7772
|
-
const parsed = JSON.parse(line);
|
|
7773
|
-
if (typeof parsed === "object" && parsed !== null)
|
|
7774
|
-
msg = parsed;
|
|
7775
|
-
} catch {}
|
|
7776
|
-
if (first) {
|
|
7777
|
-
first = false;
|
|
7778
|
-
if (msg?.type !== "system" || msg.subtype !== "init") {
|
|
7779
|
-
badStart = `claude output did not start with a system/init stream-json line (got: ${line.slice(0, 120)})`;
|
|
7780
|
-
return "kill";
|
|
7781
|
-
}
|
|
7782
|
-
if (msg.apiKeySource !== opts.keySource) {
|
|
7783
|
-
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")`;
|
|
7784
|
-
return "kill";
|
|
7785
|
-
}
|
|
7786
|
-
return;
|
|
7787
|
-
}
|
|
7788
|
-
if (msg?.type === "result")
|
|
7789
|
-
result = msg;
|
|
7790
|
-
return;
|
|
7791
|
-
}
|
|
7235
|
+
onStdout: (c) => {
|
|
7236
|
+
stdout += c.toString();
|
|
7237
|
+
},
|
|
7238
|
+
onStderr: (c) => opts.stderr.write(c)
|
|
7792
7239
|
});
|
|
7793
|
-
|
|
7794
|
-
if (spawned.spawnError) {
|
|
7795
|
-
const e = spawned.spawnError;
|
|
7796
|
-
if (e.code === "ENOENT")
|
|
7797
|
-
return {
|
|
7798
|
-
ok: false,
|
|
7799
|
-
reason: `claude binary not found: ${opts.bin} (install Claude Code, or pass --claude-bin / set FIREGOAT_CLAUDE_BIN)`
|
|
7800
|
-
};
|
|
7240
|
+
if (exited.error)
|
|
7801
7241
|
return {
|
|
7802
7242
|
ok: false,
|
|
7803
|
-
reason: `could not start ${opts.bin}: ${
|
|
7243
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`
|
|
7804
7244
|
};
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
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")
|
|
7809
7255
|
return {
|
|
7810
7256
|
ok: false,
|
|
7811
|
-
reason: `
|
|
7812
|
-
result
|
|
7257
|
+
reason: `claude did not start a session (exit ${exited.code ?? "by signal"}), see stderr.txt`
|
|
7813
7258
|
};
|
|
7814
|
-
|
|
7259
|
+
const sessionId = String(init.session_id);
|
|
7260
|
+
const result = events.findLast((e) => e?.type === "result");
|
|
7261
|
+
if (exited.timedOut)
|
|
7815
7262
|
return {
|
|
7816
7263
|
ok: false,
|
|
7817
|
-
reason: `
|
|
7264
|
+
reason: `timed out after ${opts.timeoutMs / 1000}s`,
|
|
7265
|
+
result,
|
|
7266
|
+
sessionId
|
|
7818
7267
|
};
|
|
7819
7268
|
if (!result)
|
|
7820
7269
|
return {
|
|
7821
7270
|
ok: false,
|
|
7822
|
-
reason: `claude exited ${
|
|
7271
|
+
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7272
|
+
sessionId
|
|
7823
7273
|
};
|
|
7824
7274
|
if (result.is_error === true || result.subtype !== "success")
|
|
7825
7275
|
return {
|
|
7826
7276
|
ok: false,
|
|
7827
|
-
reason: `claude result is an error (subtype=${result.subtype}
|
|
7828
|
-
result
|
|
7277
|
+
reason: `claude result is an error (subtype=${result.subtype})`,
|
|
7278
|
+
result,
|
|
7279
|
+
sessionId
|
|
7829
7280
|
};
|
|
7830
|
-
return { ok: true, result };
|
|
7281
|
+
return { ok: true, result, sessionId };
|
|
7831
7282
|
}
|
|
7832
7283
|
async function runShell(opts) {
|
|
7833
7284
|
opts.log.write(`$ ${opts.run}
|
|
7834
7285
|
`);
|
|
7835
|
-
const r = await
|
|
7286
|
+
const r = await run({
|
|
7836
7287
|
cmd: "/bin/sh",
|
|
7837
7288
|
args: ["-c", opts.run],
|
|
7838
7289
|
cwd: opts.cwd,
|
|
@@ -7842,8 +7293,8 @@ async function runShell(opts) {
|
|
|
7842
7293
|
onStderr: (c) => opts.log.write(c)
|
|
7843
7294
|
});
|
|
7844
7295
|
let error;
|
|
7845
|
-
if (r.
|
|
7846
|
-
error = `could not start /bin/sh: ${r.
|
|
7296
|
+
if (r.error)
|
|
7297
|
+
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7847
7298
|
else if (r.timedOut)
|
|
7848
7299
|
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7849
7300
|
else if (r.code === null)
|
|
@@ -7851,23 +7302,17 @@ async function runShell(opts) {
|
|
|
7851
7302
|
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7852
7303
|
|
|
7853
7304
|
`);
|
|
7854
|
-
|
|
7855
|
-
if (error)
|
|
7856
|
-
out.error = error;
|
|
7857
|
-
if (r.spawnError)
|
|
7858
|
-
out.notStarted = true;
|
|
7859
|
-
return out;
|
|
7305
|
+
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7860
7306
|
}
|
|
7861
7307
|
function checkLogin(opts) {
|
|
7862
|
-
const
|
|
7863
|
-
const r = spawnSync3(cmd, args, {
|
|
7308
|
+
const r = spawnSync(opts.bin, ["auth", "status"], {
|
|
7864
7309
|
cwd: opts.cwd,
|
|
7865
7310
|
env: opts.env,
|
|
7866
7311
|
encoding: "utf8",
|
|
7867
7312
|
timeout: 30000
|
|
7868
7313
|
});
|
|
7869
7314
|
if (r.error)
|
|
7870
|
-
return `could not run
|
|
7315
|
+
return `could not run ${opts.bin}: ${r.error.message}`;
|
|
7871
7316
|
let status;
|
|
7872
7317
|
try {
|
|
7873
7318
|
status = JSON.parse(r.stdout);
|
|
@@ -7875,56 +7320,163 @@ function checkLogin(opts) {
|
|
|
7875
7320
|
if (status?.loggedIn === true)
|
|
7876
7321
|
return;
|
|
7877
7322
|
if (status?.loggedIn === false)
|
|
7878
|
-
return
|
|
7323
|
+
return "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again";
|
|
7879
7324
|
return `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`;
|
|
7880
7325
|
}
|
|
7881
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
|
+
|
|
7882
7454
|
// src/setup.ts
|
|
7883
|
-
import {
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
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);
|
|
7888
7459
|
try {
|
|
7889
|
-
for (const run of commands) {
|
|
7460
|
+
for (const run of opts.commands) {
|
|
7890
7461
|
const r = await runShell({
|
|
7891
7462
|
run,
|
|
7892
|
-
cwd,
|
|
7893
|
-
env,
|
|
7894
|
-
timeoutMs:
|
|
7463
|
+
cwd: opts.cwd,
|
|
7464
|
+
env: opts.env,
|
|
7465
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
7895
7466
|
log
|
|
7896
7467
|
});
|
|
7897
7468
|
if (r.code !== 0)
|
|
7898
|
-
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})`;
|
|
7899
7470
|
}
|
|
7900
7471
|
return;
|
|
7901
7472
|
} finally {
|
|
7902
7473
|
await new Promise((r) => log.end(r));
|
|
7903
7474
|
}
|
|
7904
7475
|
}
|
|
7905
|
-
async function setupOnce(opts) {
|
|
7906
|
-
mkdirSync4(opts.out, { recursive: true });
|
|
7907
|
-
return runAll(opts.commands, opts.root, { ...opts.env, FIREGOAT_OUT: opts.out }, opts.logPath, "setup");
|
|
7908
|
-
}
|
|
7909
|
-
async function prepareRun(opts) {
|
|
7910
|
-
const out = join4(opts.runRoot, "out");
|
|
7911
|
-
if (opts.out === null)
|
|
7912
|
-
mkdirSync4(out);
|
|
7913
|
-
else
|
|
7914
|
-
cpSync(opts.out, out, { recursive: true });
|
|
7915
|
-
const path = [out, opts.env.PATH ?? ""].join(delimiter);
|
|
7916
|
-
const error = await runAll(opts.commands, opts.workdir, {
|
|
7917
|
-
...opts.env,
|
|
7918
|
-
PATH: path,
|
|
7919
|
-
FIREGOAT_OUT: out,
|
|
7920
|
-
FIREGOAT_WORKDIR: opts.workdir
|
|
7921
|
-
}, opts.logPath, "workdir_setup");
|
|
7922
|
-
return error ? { error } : { out, path };
|
|
7923
|
-
}
|
|
7924
7476
|
|
|
7925
7477
|
// src/which.ts
|
|
7926
7478
|
import { accessSync, constants, statSync } from "node:fs";
|
|
7927
|
-
import { delimiter
|
|
7479
|
+
import { delimiter, join as join2, resolve } from "node:path";
|
|
7928
7480
|
function resolveBin(bin) {
|
|
7929
7481
|
const executable = (p) => {
|
|
7930
7482
|
try {
|
|
@@ -7936,40 +7488,15 @@ function resolveBin(bin) {
|
|
|
7936
7488
|
};
|
|
7937
7489
|
if (bin.includes("/"))
|
|
7938
7490
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7939
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7940
|
-
if (dir && executable(
|
|
7941
|
-
return
|
|
7491
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
7492
|
+
if (dir && executable(join2(dir, bin)))
|
|
7493
|
+
return join2(dir, bin);
|
|
7942
7494
|
}
|
|
7943
7495
|
return;
|
|
7944
7496
|
}
|
|
7945
7497
|
|
|
7946
7498
|
// src/eval.ts
|
|
7947
7499
|
var CHECK_TIMEOUT_MS = 120000;
|
|
7948
|
-
var GRADING_HINT = "firegoat ran only the script checks. The goal and rubric are for the caller to judge, from each run's session (stream-json transcript) and workdir.";
|
|
7949
|
-
function blankRun(test, n, dir, prompt) {
|
|
7950
|
-
return {
|
|
7951
|
-
test: test.name,
|
|
7952
|
-
n,
|
|
7953
|
-
status: "error",
|
|
7954
|
-
reason: "",
|
|
7955
|
-
prompt,
|
|
7956
|
-
goal: test.goal,
|
|
7957
|
-
rubric: test.rubric,
|
|
7958
|
-
checks: [],
|
|
7959
|
-
dir,
|
|
7960
|
-
workdir: null,
|
|
7961
|
-
session: join6(dir, "session.jsonl"),
|
|
7962
|
-
stderr: join6(dir, "stderr.txt"),
|
|
7963
|
-
checksLog: join6(dir, "checks.txt"),
|
|
7964
|
-
num_turns: null,
|
|
7965
|
-
cost: null,
|
|
7966
|
-
duration_ms: 0
|
|
7967
|
-
};
|
|
7968
|
-
}
|
|
7969
|
-
function writeRunJson(r) {
|
|
7970
|
-
writeFileSync2(join6(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
7971
|
-
`);
|
|
7972
|
-
}
|
|
7973
7500
|
|
|
7974
7501
|
class UsageError extends Error {
|
|
7975
7502
|
}
|
|
@@ -7977,7 +7504,6 @@ function parseArgs(args) {
|
|
|
7977
7504
|
const opts = {
|
|
7978
7505
|
names: [],
|
|
7979
7506
|
json: false,
|
|
7980
|
-
host: false,
|
|
7981
7507
|
vars: {},
|
|
7982
7508
|
claudeBin: process.env.FIREGOAT_CLAUDE_BIN || "claude"
|
|
7983
7509
|
};
|
|
@@ -7996,8 +7522,6 @@ function parseArgs(args) {
|
|
|
7996
7522
|
};
|
|
7997
7523
|
if (flag === "--json")
|
|
7998
7524
|
opts.json = true;
|
|
7999
|
-
else if (flag === "--host")
|
|
8000
|
-
opts.host = true;
|
|
8001
7525
|
else if (flag === "--var") {
|
|
8002
7526
|
const kv = value();
|
|
8003
7527
|
const at = kv.indexOf("=");
|
|
@@ -8020,151 +7544,135 @@ function parseArgs(args) {
|
|
|
8020
7544
|
}
|
|
8021
7545
|
return opts;
|
|
8022
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
|
+
}
|
|
8023
7558
|
function newRunId() {
|
|
8024
7559
|
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
8025
7560
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
8026
7561
|
}
|
|
8027
|
-
function gitignoreHasFiregoat(cwd) {
|
|
8028
|
-
const file = join6(cwd, ".gitignore");
|
|
8029
|
-
if (!existsSync4(file))
|
|
8030
|
-
return false;
|
|
8031
|
-
return readFileSync3(file, "utf8").split(`
|
|
8032
|
-
`).map((l) => l.trim()).some((l) => /^\/?\.firegoat\/?(\*\*?)?$/.test(l));
|
|
8033
|
-
}
|
|
8034
|
-
function newRunRoot(label) {
|
|
8035
|
-
const runRoot = realpathSync2(mkdtempSync(join6(tmpdir(), `firegoat-${label}-`)));
|
|
8036
|
-
const workdir = join6(runRoot, "work");
|
|
8037
|
-
mkdirSync5(workdir);
|
|
8038
|
-
return { runRoot, workdir };
|
|
8039
|
-
}
|
|
8040
|
-
function isolate(iso, claudeBin, runRoot, workdir) {
|
|
8041
|
-
if (iso.host)
|
|
8042
|
-
return { profile: null };
|
|
8043
|
-
const profile = sandboxProfile({
|
|
8044
|
-
claudeBin,
|
|
8045
|
-
runRoot,
|
|
8046
|
-
configDir: claudeConfigDir(),
|
|
8047
|
-
repo: iso.repo
|
|
8048
|
-
});
|
|
8049
|
-
const broken = probeSandbox({ profile, workdir, repoFile: iso.repoFile });
|
|
8050
|
-
return broken ? { error: broken } : { profile };
|
|
8051
|
-
}
|
|
8052
7562
|
async function runOne(opts) {
|
|
8053
7563
|
const { test, n } = opts;
|
|
8054
7564
|
const started = Date.now();
|
|
8055
|
-
const dir =
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
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
|
+
};
|
|
8059
7585
|
const done = (status, reason) => {
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
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;
|
|
8065
7592
|
};
|
|
8066
|
-
const {
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
return done("error", `not graded: ${sandbox.error}`);
|
|
8071
|
-
const hostEnv = { ...process.env, ...opts.vars };
|
|
8072
|
-
let extra = {};
|
|
8073
|
-
if (opts.out !== null || opts.workdirSetup.length > 0) {
|
|
8074
|
-
const prepared = await prepareRun({
|
|
8075
|
-
out: opts.out,
|
|
8076
|
-
runRoot,
|
|
8077
|
-
workdir,
|
|
7593
|
+
const stepEnv = { ...opts.stepEnv, FIREGOAT_WORKDIR: workdir };
|
|
7594
|
+
if (opts.workdirSetup.length > 0) {
|
|
7595
|
+
const error = await runSteps({
|
|
7596
|
+
what: "workdir_setup",
|
|
8078
7597
|
commands: opts.workdirSetup,
|
|
8079
|
-
|
|
8080
|
-
|
|
7598
|
+
cwd: workdir,
|
|
7599
|
+
env: stepEnv,
|
|
7600
|
+
logPath: join3(dir, "workdir_setup.txt")
|
|
8081
7601
|
});
|
|
8082
|
-
if (
|
|
8083
|
-
return done("error", `not graded: ${
|
|
8084
|
-
if (opts.out !== null)
|
|
8085
|
-
extra = { PATH: prepared.path, FIREGOAT_OUT: prepared.out };
|
|
7602
|
+
if (error)
|
|
7603
|
+
return done("error", `not graded: ${error}`);
|
|
8086
7604
|
}
|
|
8087
|
-
const
|
|
7605
|
+
const stderr = createWriteStream2(r.stderr);
|
|
8088
7606
|
const outcome = await runClaude({
|
|
8089
|
-
env: {
|
|
8090
|
-
...agentEnv(runRoot, opts.auth.method === "api-key" ? opts.auth.apiKey : undefined),
|
|
8091
|
-
...agentPath
|
|
8092
|
-
},
|
|
8093
|
-
keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
|
|
8094
|
-
profile: sandbox.profile,
|
|
8095
7607
|
bin: opts.claudeBin,
|
|
8096
|
-
prompt:
|
|
7608
|
+
prompt: r.prompt,
|
|
8097
7609
|
cwd: workdir,
|
|
8098
|
-
|
|
8099
|
-
|
|
7610
|
+
env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
|
|
7611
|
+
stderr,
|
|
8100
7612
|
timeoutMs: test.timeout * 1000
|
|
8101
7613
|
});
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
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;
|
|
8106
7619
|
if (!outcome.ok)
|
|
8107
7620
|
return done("error", `not graded: ${outcome.reason}`);
|
|
8108
|
-
if (
|
|
8109
|
-
return done("
|
|
8110
|
-
const log =
|
|
8111
|
-
const env = {
|
|
8112
|
-
...process.env,
|
|
8113
|
-
...hostEnv,
|
|
8114
|
-
...extra,
|
|
8115
|
-
FIREGOAT_SESSION: resolve2(session),
|
|
8116
|
-
FIREGOAT_WORKDIR: workdir
|
|
8117
|
-
};
|
|
8118
|
-
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);
|
|
8119
7624
|
try {
|
|
8120
|
-
for (const c of test.checks)
|
|
8121
|
-
|
|
7625
|
+
for (const c of test.checks)
|
|
7626
|
+
r.checks.push(await runShell({
|
|
8122
7627
|
run: render(c.run, opts.vars),
|
|
8123
7628
|
cwd: workdir,
|
|
8124
|
-
env,
|
|
7629
|
+
env: { ...stepEnv, FIREGOAT_SESSION: r.session },
|
|
8125
7630
|
timeoutMs: CHECK_TIMEOUT_MS,
|
|
8126
7631
|
log
|
|
8127
7632
|
}));
|
|
8128
|
-
}
|
|
8129
7633
|
} finally {
|
|
8130
|
-
await new Promise((
|
|
7634
|
+
await new Promise((res) => log.end(res));
|
|
8131
7635
|
}
|
|
8132
|
-
|
|
8133
|
-
const broken = results.find((r) => r.notStarted);
|
|
8134
|
-
if (broken)
|
|
8135
|
-
return done("error", `not graded: check ${broken.run}: ${broken.error}`);
|
|
8136
|
-
const failed = results.filter((r) => r.code !== 0);
|
|
7636
|
+
const failed = r.checks.filter((c) => c.code !== 0);
|
|
8137
7637
|
if (failed.length > 0)
|
|
8138
|
-
return done("fail", failed.map((
|
|
8139
|
-
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`);
|
|
8140
7640
|
}
|
|
8141
|
-
async function evalCommand(args,
|
|
7641
|
+
async function evalCommand(args, root) {
|
|
8142
7642
|
if (args.includes("--help") || args.includes("-h")) {
|
|
8143
7643
|
console.log(EVAL_HELP);
|
|
8144
7644
|
return EXIT.pass;
|
|
8145
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
|
+
};
|
|
8146
7653
|
let opts;
|
|
8147
7654
|
try {
|
|
8148
7655
|
opts = parseArgs(args);
|
|
8149
7656
|
} catch (e) {
|
|
8150
|
-
if (e instanceof UsageError)
|
|
8151
|
-
|
|
8152
|
-
return EXIT.usage;
|
|
8153
|
-
}
|
|
7657
|
+
if (e instanceof UsageError)
|
|
7658
|
+
return fatal(EXIT.usage, e.message);
|
|
8154
7659
|
throw e;
|
|
8155
7660
|
}
|
|
8156
7661
|
const say = opts.json ? console.error : console.log;
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
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
|
+
}
|
|
8160
7670
|
let tests = config.tests;
|
|
8161
7671
|
if (opts.names.length > 0) {
|
|
8162
7672
|
const known = new Set(tests.map((t) => t.name));
|
|
8163
7673
|
const unknown = opts.names.filter((n) => !known.has(n));
|
|
8164
|
-
if (unknown.length > 0)
|
|
8165
|
-
|
|
8166
|
-
return EXIT.usage;
|
|
8167
|
-
}
|
|
7674
|
+
if (unknown.length > 0)
|
|
7675
|
+
return fatal(EXIT.usage, `unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
|
|
8168
7676
|
tests = tests.filter((t) => opts.names.includes(t.name));
|
|
8169
7677
|
}
|
|
8170
7678
|
const runs = opts.runs ?? config.runs;
|
|
@@ -8172,572 +7680,204 @@ async function evalCommand(args, cwd) {
|
|
|
8172
7680
|
const missing = [
|
|
8173
7681
|
...new Set(tests.flatMap((t) => [t.prompt, ...t.checks.map((c) => c.run)]).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
8174
7682
|
];
|
|
8175
|
-
if (missing.length > 0)
|
|
8176
|
-
|
|
8177
|
-
return EXIT.usage;
|
|
8178
|
-
}
|
|
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`);
|
|
8179
7685
|
const claudePath = resolveBin(opts.claudeBin);
|
|
8180
|
-
if (!claudePath)
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
else if (process.platform !== "darwin" || !existsSync4(SANDBOX_EXEC)) {
|
|
8187
|
-
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.`);
|
|
8188
|
-
return EXIT.usage;
|
|
8189
|
-
}
|
|
8190
|
-
const iso = {
|
|
8191
|
-
host: opts.host,
|
|
8192
|
-
repo: root,
|
|
8193
|
-
repoFile: join6(root, "firegoat.yaml")
|
|
8194
|
-
};
|
|
8195
|
-
let auth;
|
|
8196
|
-
try {
|
|
8197
|
-
auth = resolveAuth(process.env);
|
|
8198
|
-
} catch (e) {
|
|
8199
|
-
if (e instanceof AuthError) {
|
|
8200
|
-
console.error(`firegoat eval: ${e.message}`);
|
|
8201
|
-
return EXIT.usage;
|
|
8202
|
-
}
|
|
8203
|
-
throw e;
|
|
8204
|
-
}
|
|
8205
|
-
say(`auth: ${auth.label}`);
|
|
8206
|
-
if (auth.method === "claude" && process.env.ANTHROPIC_API_KEY)
|
|
8207
|
-
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`)");
|
|
8208
|
-
mkdirSync5(claudeConfigDir(), { recursive: true });
|
|
8209
|
-
const pre = newRunRoot("preflight");
|
|
8210
|
-
const preSandbox = isolate(iso, claudePath, pre.runRoot, pre.workdir);
|
|
8211
|
-
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({
|
|
8212
7692
|
bin: claudePath,
|
|
8213
|
-
env:
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
console.error(`firegoat eval: not graded: ${loginProblem}`);
|
|
8219
|
-
return EXIT.notGraded;
|
|
8220
|
-
}
|
|
8221
|
-
mkdirSync5(join6(root, ".firegoat"), { recursive: true });
|
|
8222
|
-
if (!gitignoreHasFiregoat(root))
|
|
8223
|
-
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}`);
|
|
8224
7698
|
const runId = newRunId();
|
|
8225
|
-
const runDir =
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
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
|
+
};
|
|
8229
7708
|
if (config.setup.length > 0) {
|
|
8230
7709
|
say("setup...");
|
|
8231
|
-
|
|
8232
|
-
|
|
7710
|
+
const error = await runSteps({
|
|
7711
|
+
what: "setup",
|
|
8233
7712
|
commands: config.setup,
|
|
8234
|
-
root,
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
logPath: join6(runDir, "setup.txt")
|
|
7713
|
+
cwd: root,
|
|
7714
|
+
env: stepEnv,
|
|
7715
|
+
logPath: join3(runDir, "setup.txt")
|
|
8238
7716
|
});
|
|
8239
|
-
if (error)
|
|
8240
|
-
|
|
8241
|
-
return EXIT.notGraded;
|
|
8242
|
-
}
|
|
7717
|
+
if (error)
|
|
7718
|
+
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
8243
7719
|
}
|
|
8244
7720
|
const results = [];
|
|
8245
|
-
const
|
|
8246
|
-
const totals = { pass: 0, fail: 0, error: 0 };
|
|
8247
|
-
for (const r of results)
|
|
8248
|
-
totals[r.status]++;
|
|
8249
|
-
const doc = {
|
|
8250
|
-
runId,
|
|
8251
|
-
runsDir: runDir,
|
|
8252
|
-
isolation: opts.host ? "host" : "seatbelt",
|
|
8253
|
-
auth: auth.method,
|
|
8254
|
-
grading: GRADING_HINT,
|
|
8255
|
-
totals,
|
|
8256
|
-
runs: results
|
|
8257
|
-
};
|
|
8258
|
-
writeFileSync2(summaryPath, `${JSON.stringify(doc, null, 2)}
|
|
8259
|
-
`);
|
|
8260
|
-
return doc;
|
|
8261
|
-
};
|
|
8262
|
-
for (const test of tests) {
|
|
7721
|
+
for (const test of tests)
|
|
8263
7722
|
for (let n = 1;n <= runs; n++) {
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
workdirSetup: config.workdirSetup,
|
|
8275
|
-
vars,
|
|
8276
|
-
warn
|
|
8277
|
-
});
|
|
8278
|
-
} catch (e) {
|
|
8279
|
-
const dir = join6(runDir, test.name, String(n));
|
|
8280
|
-
mkdirSync5(dir, { recursive: true });
|
|
8281
|
-
r = blankRun(test, n, dir, test.prompt);
|
|
8282
|
-
r.reason = `not graded: firegoat crashed: ${e.message}`;
|
|
8283
|
-
writeRunJson(r);
|
|
8284
|
-
}
|
|
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
|
+
});
|
|
8285
7733
|
results.push(r);
|
|
8286
|
-
writeSummary();
|
|
8287
7734
|
say(`${r.status.toUpperCase()} ${r.test}#${r.n} ${r.reason}`);
|
|
8288
7735
|
}
|
|
8289
|
-
}
|
|
8290
|
-
const
|
|
8291
|
-
|
|
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
|
+
`);
|
|
8292
7742
|
say(`
|
|
8293
|
-
${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
|
|
8294
|
-
say(`runs: ${runDir}`);
|
|
8295
|
-
say(`goal and rubric are 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)`);
|
|
8296
7745
|
if (opts.json)
|
|
8297
7746
|
console.log(JSON.stringify(doc, null, 2));
|
|
8298
|
-
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;
|
|
8299
7748
|
}
|
|
8300
7749
|
|
|
8301
7750
|
// src/init.ts
|
|
8302
|
-
import { spawnSync as
|
|
8303
|
-
import { existsSync as
|
|
8304
|
-
import { basename
|
|
8305
|
-
import { createInterface
|
|
8306
|
-
import { Writable } from "node:stream";
|
|
8307
|
-
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";
|
|
8308
7755
|
var SKILLS_ARGV = [
|
|
8309
7756
|
"skills",
|
|
8310
7757
|
"add",
|
|
8311
|
-
"
|
|
8312
|
-
|
|
7758
|
+
"firegoat-ai/skills",
|
|
7759
|
+
"--skill",
|
|
7760
|
+
"firegoat-onboarding",
|
|
7761
|
+
"--skill",
|
|
7762
|
+
"suggesting-eval-prompts",
|
|
8313
7763
|
"--agent",
|
|
8314
7764
|
"claude-code",
|
|
8315
7765
|
"-y"
|
|
8316
7766
|
];
|
|
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
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
else if (pkg.bin && typeof pkg.bin === "object")
|
|
8357
|
-
bin = Object.keys(pkg.bin)[0];
|
|
8358
|
-
const library = !bin || pkg.main !== undefined || pkg.exports !== undefined || pkg.module !== undefined;
|
|
8359
|
-
const docsUrl = typeof pkg.homepage === "string" && /^https?:\/\//.test(pkg.homepage) ? pkg.homepage : undefined;
|
|
8360
|
-
return { ecosystem: "npm", name, bin, library, readme, docsUrl };
|
|
8361
|
-
} catch {}
|
|
8362
|
-
}
|
|
8363
|
-
const pyText = readText(join7(cwd, "pyproject.toml"));
|
|
8364
|
-
if (pyText !== undefined) {
|
|
8365
|
-
const s = tomlSections(pyText);
|
|
8366
|
-
const name = s.get("project")?.get("name") ?? s.get("tool.poetry")?.get("name") ?? fallback.name;
|
|
8367
|
-
const scripts = s.get("project.scripts") ?? s.get("tool.poetry.scripts");
|
|
8368
|
-
const bin = scripts ? [...scripts.keys()][0] : undefined;
|
|
8369
|
-
const urls = s.get("project.urls");
|
|
8370
|
-
const docsUrl = urls?.get("Documentation") ?? urls?.get("documentation") ?? urls?.get("Homepage") ?? urls?.get("homepage");
|
|
8371
|
-
return { ecosystem: "python", name, bin, library: true, readme, docsUrl };
|
|
8372
|
-
}
|
|
8373
|
-
const goText = readText(join7(cwd, "go.mod"));
|
|
8374
|
-
if (goText !== undefined) {
|
|
8375
|
-
const name = goText.match(/^module\s+(\S+)/m)?.[1] ?? fallback.name;
|
|
8376
|
-
return { ecosystem: "go", name, library: true, readme };
|
|
8377
|
-
}
|
|
8378
|
-
const cargoText = readText(join7(cwd, "Cargo.toml"));
|
|
8379
|
-
if (cargoText !== undefined) {
|
|
8380
|
-
const name = tomlSections(cargoText).get("package")?.get("name");
|
|
8381
|
-
return {
|
|
8382
|
-
ecosystem: "rust",
|
|
8383
|
-
name: name ?? fallback.name,
|
|
8384
|
-
library: true,
|
|
8385
|
-
readme
|
|
8386
|
-
};
|
|
8387
|
-
}
|
|
8388
|
-
return fallback;
|
|
8389
|
-
}
|
|
8390
|
-
var LANG = {
|
|
8391
|
-
npm: "Node.js",
|
|
8392
|
-
python: "Python",
|
|
8393
|
-
go: "Go",
|
|
8394
|
-
rust: "Rust"
|
|
8395
|
-
};
|
|
8396
|
-
function importName(p) {
|
|
8397
|
-
if (p.ecosystem === "python" || p.ecosystem === "rust")
|
|
8398
|
-
return p.name.replace(/-/g, "_");
|
|
8399
|
-
return p.name;
|
|
8400
|
-
}
|
|
8401
|
-
var GREP_EXCLUDES = [
|
|
8402
|
-
"--exclude=output.txt",
|
|
8403
|
-
"--exclude=package.json",
|
|
8404
|
-
"--exclude=package-lock.json",
|
|
8405
|
-
"--exclude=pyproject.toml",
|
|
8406
|
-
"--exclude=requirements.txt",
|
|
8407
|
-
"--exclude=go.mod",
|
|
8408
|
-
"--exclude=go.sum",
|
|
8409
|
-
"--exclude=Cargo.toml",
|
|
8410
|
-
"--exclude=Cargo.lock",
|
|
8411
|
-
"--exclude-dir=node_modules",
|
|
8412
|
-
"--exclude-dir=.venv",
|
|
8413
|
-
"--exclude-dir=target"
|
|
8414
|
-
].join(" ");
|
|
8415
|
-
function sq(s) {
|
|
8416
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
8417
|
-
}
|
|
8418
|
-
function templates(p) {
|
|
8419
|
-
const docs = p.docsUrl ? `its docs at ${p.docsUrl}` : "its official docs";
|
|
8420
|
-
const out = [];
|
|
8421
|
-
if (p.bin) {
|
|
8422
|
-
out.push({
|
|
8423
|
-
name: "cli-first-run",
|
|
8424
|
-
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.`,
|
|
8425
|
-
goal: `cli-output.txt exists and holds real output from running \`${p.bin}\`, not an install log or an error.`,
|
|
8426
|
-
checks: [
|
|
8427
|
-
"test -s cli-output.txt",
|
|
8428
|
-
"! grep -qiE 'command not found|not recognized|ENOENT' cli-output.txt"
|
|
8429
|
-
]
|
|
8430
|
-
});
|
|
8431
|
-
}
|
|
8432
|
-
if (p.library && p.ecosystem) {
|
|
8433
|
-
out.push({
|
|
8434
|
-
name: "quickstart",
|
|
8435
|
-
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.`,
|
|
8436
|
-
goal: `A program in the workdir uses ${p.name}, runs without errors, and its output is saved in output.txt.`,
|
|
8437
|
-
checks: [
|
|
8438
|
-
"test -s output.txt",
|
|
8439
|
-
`grep -rqsF ${GREP_EXCLUDES} -- ${sq(importName(p))} .`
|
|
8440
|
-
]
|
|
8441
|
-
});
|
|
8442
|
-
}
|
|
8443
|
-
if (out.length === 0) {
|
|
8444
|
-
out.push({
|
|
8445
|
-
name: "quickstart",
|
|
8446
|
-
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.`,
|
|
8447
|
-
goal: `The ${p.name} quickstart example runs without errors and its output is saved in output.txt.`,
|
|
8448
|
-
checks: ["test -s output.txt"]
|
|
8449
|
-
});
|
|
8450
|
-
}
|
|
8451
|
-
return out;
|
|
8452
|
-
}
|
|
8453
|
-
function block(text, indent) {
|
|
8454
|
-
return text.split(`
|
|
8455
|
-
`).map((l) => indent + l).join(`
|
|
8456
|
-
`);
|
|
8457
|
-
}
|
|
8458
|
-
function renderYaml(p, tests) {
|
|
8459
|
-
const lines = [
|
|
8460
|
-
`# firegoat.yaml: evals for how well coding agents use ${p.name}.`,
|
|
8461
|
-
"# Each test runs a coding agent in an empty workdir with `prompt` as the first message.",
|
|
8462
|
-
"# These are starter templates: edit the prompts to match real user problems.",
|
|
8463
|
-
"# Good prompts state one concrete outcome as a user would, not implementation steps,",
|
|
8464
|
-
"# and ask for observable proof (stdout or a file) that `checks` can verify.",
|
|
8465
|
-
"",
|
|
8466
|
-
"# Agents to evaluate (v1 supports: claude-code).",
|
|
8467
|
-
"agents: [claude-code]",
|
|
8468
|
-
"# How many times to run each test.",
|
|
8469
|
-
"runs: 1",
|
|
8470
|
-
"",
|
|
8471
|
-
"# Optional: test your local build instead of the published package.",
|
|
8472
|
-
"# setup: runs once per eval, in this folder, on your machine. Whatever it writes to",
|
|
8473
|
-
"# $FIREGOAT_OUT is copied into each run's folder and put on the agent's PATH.",
|
|
8474
|
-
"# workdir_setup: runs in each fresh workdir before the agent starts. Then tell the",
|
|
8475
|
-
'# agent in the prompt: "The package is already installed from the build under test."',
|
|
8476
|
-
"#",
|
|
8477
|
-
"# npm:",
|
|
8478
|
-
'# setup: npm pack --pack-destination "$FIREGOAT_OUT"',
|
|
8479
|
-
'# workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz',
|
|
8480
|
-
"#",
|
|
8481
|
-
"# python:",
|
|
8482
|
-
'# setup: uv build --wheel --out-dir "$FIREGOAT_OUT"',
|
|
8483
|
-
'# workdir_setup: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl',
|
|
8484
|
-
"#",
|
|
8485
|
-
"# a CLI binary (lands on PATH as is):",
|
|
8486
|
-
'# setup: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli',
|
|
8487
|
-
"#",
|
|
8488
|
-
"# Values for {{vars.KEY}} in prompts and checks, overridden by `--var KEY=value`;",
|
|
8489
|
-
"# setup commands and checks also get them as env vars.",
|
|
8490
|
-
"# vars:",
|
|
8491
|
-
"# SHA: main",
|
|
8492
|
-
"",
|
|
8493
|
-
"tests:"
|
|
8494
|
-
];
|
|
8495
|
-
for (const t of tests) {
|
|
8496
|
-
lines.push(` - name: ${t.name} # letters, digits, '.', '_' and '-'`, " # What a user would ask the agent. Edit this.", " prompt: >-", block(t.prompt, " "), " # One sentence that is true when the user's problem is solved.", " goal: >-", block(t.goal, " "), " # Shell commands run in the agent's workdir afterwards; exit 0 = pass.", " checks:", ...t.checks.map((c) => ` - run: ${JSON.stringify(c)}`), " # Seconds before the agent run is stopped.", " timeout: 600");
|
|
8497
|
-
}
|
|
8498
|
-
return `${lines.join(`
|
|
8499
|
-
`)}
|
|
7767
|
+
function template(product) {
|
|
7768
|
+
return `# firegoat.yaml: evals for how well coding agents use ${product}.
|
|
7769
|
+
# Each test runs your claude in an empty folder (.firegoat/runs/...) with \`prompt\`
|
|
7770
|
+
# as the first message, then runs \`checks\` there. Reference: npx firegoat init --help
|
|
7771
|
+
|
|
7772
|
+
# How many times to run each test.
|
|
7773
|
+
runs: 1
|
|
7774
|
+
|
|
7775
|
+
# Optional: test your local build instead of the published package.
|
|
7776
|
+
# setup: runs once per eval, here; what it writes to $FIREGOAT_OUT is put on the
|
|
7777
|
+
# agent's PATH. workdir_setup: runs in each fresh folder before the agent starts.
|
|
7778
|
+
#
|
|
7779
|
+
# npm:
|
|
7780
|
+
# setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7781
|
+
# workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7782
|
+
#
|
|
7783
|
+
# python:
|
|
7784
|
+
# setup: uv build --wheel --out-dir "$FIREGOAT_OUT"
|
|
7785
|
+
# workdir_setup: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl
|
|
7786
|
+
#
|
|
7787
|
+
# a CLI binary:
|
|
7788
|
+
# setup: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli
|
|
7789
|
+
|
|
7790
|
+
tests:
|
|
7791
|
+
- name: quickstart
|
|
7792
|
+
# What a user would ask their agent. Edit this.
|
|
7793
|
+
prompt: >-
|
|
7794
|
+
I want to try ${product}. Get the smallest working example going in a new
|
|
7795
|
+
project, run it, and save what it prints to output.txt. Keep it minimal.
|
|
7796
|
+
# True when the user's problem is solved. Not graded by firegoat: it goes to
|
|
7797
|
+
# your agent (eval --json, run.json) to judge.
|
|
7798
|
+
end_goal: >-
|
|
7799
|
+
A minimal project uses ${product}, runs without errors, and output.txt holds
|
|
7800
|
+
its output.
|
|
7801
|
+
# Shell commands run in the agent's folder afterwards; exit 0 = pass.
|
|
7802
|
+
checks:
|
|
7803
|
+
- run: test -s output.txt
|
|
7804
|
+
# Seconds before the agent run is stopped.
|
|
7805
|
+
timeout: 600
|
|
8500
7806
|
`;
|
|
8501
7807
|
}
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
return
|
|
8513
|
-
const sep = text === "" || text.endsWith(`
|
|
8514
|
-
`) ? "" : `
|
|
8515
|
-
`;
|
|
8516
|
-
writeFileSync3(file, `${text}${sep}${GITIGNORE_LINE}
|
|
8517
|
-
`);
|
|
8518
|
-
return `added ${GITIGNORE_LINE} to .gitignore`;
|
|
8519
|
-
}
|
|
8520
|
-
function installSkills(cwd) {
|
|
8521
|
-
const r = spawnSync4("npx", ["-y", ...SKILLS_ARGV], {
|
|
8522
|
-
cwd,
|
|
8523
|
-
stdio: "inherit"
|
|
8524
|
-
});
|
|
8525
|
-
if (r.error)
|
|
8526
|
-
return r.error.message;
|
|
8527
|
-
if (r.status !== 0)
|
|
8528
|
-
return `exit ${r.status ?? "by signal"}`;
|
|
8529
|
-
return;
|
|
8530
|
-
}
|
|
8531
|
-
async function ask(question) {
|
|
8532
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8533
|
-
try {
|
|
8534
|
-
return (await rl.question(question)).trim();
|
|
8535
|
-
} finally {
|
|
8536
|
-
rl.close();
|
|
8537
|
-
}
|
|
8538
|
-
}
|
|
8539
|
-
async function readSecret(question) {
|
|
8540
|
-
if (!process.stdin.isTTY) {
|
|
8541
|
-
const chunks = [];
|
|
8542
|
-
for await (const c of process.stdin)
|
|
8543
|
-
chunks.push(c);
|
|
8544
|
-
return Buffer.concat(chunks).toString("utf8").trim();
|
|
8545
|
-
}
|
|
8546
|
-
process.stdout.write(question);
|
|
8547
|
-
const silent = new Writable({ write: (_c, _e, cb) => cb() });
|
|
8548
|
-
const rl = createInterface2({
|
|
8549
|
-
input: process.stdin,
|
|
8550
|
-
output: silent,
|
|
8551
|
-
terminal: true
|
|
7808
|
+
var AGENT_PROMPT = "Scan my project and setup 2-3 agent experience prompts to test general usage of the app. Use firegoat skills";
|
|
7809
|
+
var CLIPBOARD = [
|
|
7810
|
+
["pbcopy", []],
|
|
7811
|
+
["wl-copy", []],
|
|
7812
|
+
["xclip", ["-selection", "clipboard"]],
|
|
7813
|
+
["clip", []]
|
|
7814
|
+
];
|
|
7815
|
+
function copyToClipboard(text) {
|
|
7816
|
+
return CLIPBOARD.some(([cmd, args]) => {
|
|
7817
|
+
const r = spawnSync2(cmd, args, { input: text });
|
|
7818
|
+
return !r.error && r.status === 0;
|
|
8552
7819
|
});
|
|
8553
|
-
try {
|
|
8554
|
-
return (await rl.question("")).trim();
|
|
8555
|
-
} finally {
|
|
8556
|
-
rl.close();
|
|
8557
|
-
process.stdout.write(`
|
|
8558
|
-
`);
|
|
8559
|
-
}
|
|
8560
7820
|
}
|
|
8561
|
-
async function
|
|
8562
|
-
|
|
7821
|
+
async function askYes(question) {
|
|
7822
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8563
7823
|
try {
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
current = e.message;
|
|
8567
|
-
}
|
|
8568
|
-
console.log(`How should the agent sign in? (now: ${current})
|
|
8569
|
-
1) Login with Claude (your Claude plan)
|
|
8570
|
-
2) Paste an Anthropic API key`);
|
|
8571
|
-
for (;; ) {
|
|
8572
|
-
const a = await ask("choose [1/2]: ");
|
|
8573
|
-
if (a === "1")
|
|
8574
|
-
return "claude";
|
|
8575
|
-
if (a === "2")
|
|
8576
|
-
return "api-key";
|
|
8577
|
-
console.log("type 1 or 2");
|
|
8578
|
-
}
|
|
8579
|
-
}
|
|
8580
|
-
async function setUpAuth(method) {
|
|
8581
|
-
if (method === "claude") {
|
|
8582
|
-
const code = claudeLoginCommand([]);
|
|
8583
|
-
return code === 0 ? undefined : code;
|
|
8584
|
-
}
|
|
8585
|
-
const key = await readSecret("Anthropic API key (input hidden): ");
|
|
8586
|
-
if (!looksLikeAnthropicKey(key)) {
|
|
8587
|
-
console.error("firegoat init: that does not look like an Anthropic API key (sk-ant-...); nothing stored");
|
|
8588
|
-
return EXIT.usage;
|
|
8589
|
-
}
|
|
8590
|
-
const file = saveApiKey(key);
|
|
8591
|
-
console.log(`stored the API key in ${file} (mode 0600, not in the repo); evals pass it to the agent as ANTHROPIC_API_KEY`);
|
|
8592
|
-
return;
|
|
8593
|
-
}
|
|
8594
|
-
async function askYesNo(question) {
|
|
8595
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8596
|
-
try {
|
|
8597
|
-
const answer = await rl.question(`${question} [y/N] `);
|
|
8598
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
7824
|
+
const a = (await rl.question(`${question} [Y/n] `)).trim();
|
|
7825
|
+
return a === "" || /^y(es)?$/i.test(a);
|
|
8599
7826
|
} finally {
|
|
8600
7827
|
rl.close();
|
|
8601
7828
|
}
|
|
8602
7829
|
}
|
|
8603
|
-
function describe(p) {
|
|
8604
|
-
if (!p.ecosystem)
|
|
8605
|
-
return "no package manifest found, writing a generic test";
|
|
8606
|
-
const parts = [`${p.ecosystem} package ${p.name}`];
|
|
8607
|
-
if (p.bin)
|
|
8608
|
-
parts.push(`bin: ${p.bin}`);
|
|
8609
|
-
if (p.readme)
|
|
8610
|
-
parts.push(`docs: ${p.readme}`);
|
|
8611
|
-
return parts.join(", ");
|
|
8612
|
-
}
|
|
8613
7830
|
async function initCommand(args, cwd) {
|
|
8614
|
-
let yes = false;
|
|
8615
7831
|
let force = false;
|
|
8616
|
-
let skills =
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
}
|
|
8629
|
-
switch (a) {
|
|
8630
|
-
case "--yes":
|
|
8631
|
-
case "-y":
|
|
8632
|
-
yes = true;
|
|
8633
|
-
break;
|
|
8634
|
-
case "--force":
|
|
8635
|
-
case "-f":
|
|
8636
|
-
force = true;
|
|
8637
|
-
break;
|
|
8638
|
-
case "--skills":
|
|
8639
|
-
skills = true;
|
|
8640
|
-
break;
|
|
8641
|
-
case "--help":
|
|
8642
|
-
case "-h":
|
|
8643
|
-
console.log(INIT_HELP);
|
|
8644
|
-
return 0;
|
|
8645
|
-
default:
|
|
8646
|
-
console.error(`firegoat init: unknown argument: ${a}
|
|
7832
|
+
let skills = true;
|
|
7833
|
+
for (const a of args) {
|
|
7834
|
+
if (a === "--help" || a === "-h") {
|
|
7835
|
+
console.log(INIT_HELP);
|
|
7836
|
+
return EXIT.pass;
|
|
7837
|
+
}
|
|
7838
|
+
if (a === "--force" || a === "-f")
|
|
7839
|
+
force = true;
|
|
7840
|
+
else if (a === "--no-skills")
|
|
7841
|
+
skills = false;
|
|
7842
|
+
else {
|
|
7843
|
+
console.error(`firegoat init: unknown argument: ${a}
|
|
8647
7844
|
|
|
8648
7845
|
${INIT_HELP}`);
|
|
8649
|
-
|
|
8650
|
-
}
|
|
8651
|
-
}
|
|
8652
|
-
|
|
8653
|
-
const
|
|
8654
|
-
if (
|
|
8655
|
-
console.
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
console.log(`detected: ${describe(project)}`);
|
|
8664
|
-
writeFileSync3(configPath, renderYaml(project, tests));
|
|
8665
|
-
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`);
|
|
8666
|
-
}
|
|
8667
|
-
console.log(ensureGitignore(cwd));
|
|
8668
|
-
const interactive = !yes && process.stdin.isTTY && process.stdout.isTTY;
|
|
8669
|
-
if (!auth && interactive)
|
|
8670
|
-
auth = await chooseAuth();
|
|
8671
|
-
if (auth) {
|
|
8672
|
-
const failed = await setUpAuth(auth);
|
|
8673
|
-
if (failed !== undefined)
|
|
8674
|
-
return failed;
|
|
8675
|
-
}
|
|
8676
|
-
if (!skills && interactive)
|
|
8677
|
-
skills = await askYesNo(`Add the ${SKILLS.join(" and ")} skills to .claude/skills/ (help your agent draft better evals)?`);
|
|
7846
|
+
return EXIT.usage;
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
ensureFiregoatDir(cwd);
|
|
7850
|
+
const configPath = join4(cwd, CONFIG_FILE);
|
|
7851
|
+
if (existsSync4(configPath) && !force)
|
|
7852
|
+
console.log(`kept ${CONFIG_FILE} (--force replaces it)`);
|
|
7853
|
+
else {
|
|
7854
|
+
writeFileSync3(configPath, template(basename(cwd)));
|
|
7855
|
+
console.log(`wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
7856
|
+
}
|
|
7857
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
7858
|
+
if (skills && interactive)
|
|
7859
|
+
skills = await askYes("Install the firegoat-onboarding skill for your coding agent?");
|
|
8678
7860
|
if (skills) {
|
|
8679
7861
|
console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8680
|
-
const
|
|
8681
|
-
|
|
8682
|
-
|
|
7862
|
+
const env = { ...process.env };
|
|
7863
|
+
delete env.npm_config_package;
|
|
7864
|
+
const r = spawnSync2("npx", ["-y", ...SKILLS_ARGV], {
|
|
7865
|
+
cwd,
|
|
7866
|
+
env,
|
|
7867
|
+
stdio: "inherit"
|
|
7868
|
+
});
|
|
7869
|
+
if (r.error || r.status !== 0) {
|
|
7870
|
+
console.error(`firegoat init: installing the skills failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8683
7871
|
return EXIT.notGraded;
|
|
8684
7872
|
}
|
|
8685
|
-
} else {
|
|
8686
|
-
console.log(`hint: \`npx firegoat init --skills\` adds the ${SKILLS.join(" and ")} skills to .claude/skills/ (keeps your ${CONFIG_FILE})`);
|
|
8687
7873
|
}
|
|
8688
|
-
console.log(
|
|
8689
|
-
|
|
8690
|
-
}
|
|
7874
|
+
console.log(`
|
|
7875
|
+
Paste this into your coding agent:
|
|
8691
7876
|
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
var OPENER = {
|
|
8697
|
-
darwin: "open",
|
|
8698
|
-
linux: "xdg-open"
|
|
8699
|
-
};
|
|
8700
|
-
function openCommand(args, cwd) {
|
|
8701
|
-
let print = false;
|
|
8702
|
-
const ids = [];
|
|
8703
|
-
for (const a of args) {
|
|
8704
|
-
if (a === "--help" || a === "-h") {
|
|
8705
|
-
console.log(OPEN_HELP);
|
|
8706
|
-
return EXIT.pass;
|
|
8707
|
-
}
|
|
8708
|
-
if (a === "--print")
|
|
8709
|
-
print = true;
|
|
8710
|
-
else if (a.startsWith("-")) {
|
|
8711
|
-
console.error(`firegoat open: unknown flag: ${a}`);
|
|
8712
|
-
return EXIT.usage;
|
|
8713
|
-
} else
|
|
8714
|
-
ids.push(a);
|
|
8715
|
-
}
|
|
8716
|
-
if (ids.length > 1) {
|
|
8717
|
-
console.error("firegoat open: takes at most one run id");
|
|
8718
|
-
return EXIT.usage;
|
|
8719
|
-
}
|
|
8720
|
-
const base = join8(findConfigDir(cwd), ".firegoat", "runs");
|
|
8721
|
-
const runs = existsSync6(base) ? readdirSync(base).sort() : [];
|
|
8722
|
-
const id = ids[0] ?? runs.at(-1);
|
|
8723
|
-
if (!id) {
|
|
8724
|
-
console.error(`firegoat open: no runs in ${base}. Run: npx firegoat eval`);
|
|
8725
|
-
return EXIT.usage;
|
|
8726
|
-
}
|
|
8727
|
-
if (!runs.includes(id)) {
|
|
8728
|
-
console.error(`firegoat open: no run ${JSON.stringify(id)} in ${base} (latest: ${runs.at(-1) ?? "none"})`);
|
|
8729
|
-
return EXIT.usage;
|
|
8730
|
-
}
|
|
8731
|
-
const dir = join8(base, id);
|
|
8732
|
-
console.log(dir);
|
|
8733
|
-
const opener = OPENER[process.platform];
|
|
8734
|
-
if (print || !opener)
|
|
8735
|
-
return EXIT.pass;
|
|
8736
|
-
const r = spawnSync5(opener, [dir], { stdio: "inherit" });
|
|
8737
|
-
if (r.error || r.status !== 0) {
|
|
8738
|
-
console.error(`firegoat open: ${opener} failed (${r.error?.message ?? `exit ${r.status}`}); the run is at the path above`);
|
|
8739
|
-
return EXIT.notGraded;
|
|
8740
|
-
}
|
|
7877
|
+
${AGENT_PROMPT}
|
|
7878
|
+
`);
|
|
7879
|
+
if (interactive && copyToClipboard(AGENT_PROMPT))
|
|
7880
|
+
console.log("\x1B[32mPrompt copied!\x1B[0m");
|
|
8741
7881
|
return EXIT.pass;
|
|
8742
7882
|
}
|
|
8743
7883
|
|
|
@@ -8756,24 +7896,8 @@ async function main(argv) {
|
|
|
8756
7896
|
return 0;
|
|
8757
7897
|
case "init":
|
|
8758
7898
|
return initCommand(rest, process.cwd());
|
|
8759
|
-
case "claude":
|
|
8760
|
-
if (rest[0] === "login")
|
|
8761
|
-
return claudeLoginCommand(rest.slice(1));
|
|
8762
|
-
if (rest[0] === "--help" || rest[0] === "-h") {
|
|
8763
|
-
console.log(CLAUDE_LOGIN_HELP);
|
|
8764
|
-
return 0;
|
|
8765
|
-
}
|
|
8766
|
-
console.error(`firegoat claude: unknown subcommand
|
|
8767
|
-
|
|
8768
|
-
${CLAUDE_LOGIN_HELP}`);
|
|
8769
|
-
return EXIT.usage;
|
|
8770
|
-
case "login":
|
|
8771
|
-
console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
|
|
8772
|
-
return claudeLoginCommand(rest);
|
|
8773
7899
|
case "eval":
|
|
8774
7900
|
return evalCommand(rest, process.cwd());
|
|
8775
|
-
case "open":
|
|
8776
|
-
return openCommand(rest, process.cwd());
|
|
8777
7901
|
default:
|
|
8778
7902
|
console.error(`unknown command: ${cmd}
|
|
8779
7903
|
|