firegoat 0.10.0 → 0.12.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 +21 -2
- package/dist/cli.js +444 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local AX evals: how well coding agents use your SDK, CLI, API and docs. firegoat gives your own `claude` one prompt in a fresh folder, saves the session, and runs your checks.
|
|
4
4
|
|
|
5
|
-
Status: early (v0.
|
|
5
|
+
Status: early (v0.11). See `plan.md`.
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
@@ -82,14 +82,33 @@ tests:
|
|
|
82
82
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Env vars for the agent: a home dir per run, a feature flag
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
env:
|
|
89
|
+
ACME_HOME: "/tmp/acme-{{run.id}}-{{run.n}}" # sessions/config never collide across runs
|
|
90
|
+
ACME_TELEMETRY: "off"
|
|
91
|
+
tests:
|
|
92
|
+
- name: login
|
|
93
|
+
prompt: Log in to acme with the CLI and print the current user.
|
|
94
|
+
end_goal: The agent completes acme's login flow and prints the user.
|
|
95
|
+
env:
|
|
96
|
+
ACME_BETA_LOGIN: "1" # this test only, on top of env:
|
|
97
|
+
checks:
|
|
98
|
+
- run: ls "$ACME_HOME"/sessions | grep -q .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`env:` goes to the agent, and to `workdir_setup` and checks of the same run, so checks see the same values. Placeholders: `{{vars.KEY}}`, `{{run.id}}` (the eval's run id), `{{run.n}}` (1..runs), `{{run.workdir}}`, `{{test.name}}`. `setup` runs once, before any run: it gets only the top-level entries that use nothing but `{{vars.*}}` and `{{run.id}}`. Values are strings (quote numbers). `PATH`, `CLAUDE_CONFIG_DIR` and `FIREGOAT_*` are firegoat's and can't be set.
|
|
102
|
+
|
|
85
103
|
## What eval does
|
|
86
104
|
|
|
87
105
|
- claude runs on its own login (or `ANTHROPIC_API_KEY`, if set) with `CLAUDE_CONFIG_DIR=.firegoat/.claude` and `--setting-sources "" --strict-mcp-config --disable-slash-commands`: none of your settings, CLAUDE.md, plugins, skills or MCP servers. claude saves its session there, in its own format.
|
|
88
106
|
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. In `.firegoat/`, git ignores `runs/` and `.claude/`; keep test fixtures (scripts, sample data) in `.firegoat/fixtures/` and commit them with `firegoat.yaml`.
|
|
107
|
+
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, setup, workdir_setup, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it.
|
|
89
108
|
- Script checks run in the agent's folder afterwards and set the exit code. `end_goal` is not graded by firegoat: it's for you, or your coding agent, to judge from `run.json`, the session and the folder.
|
|
90
109
|
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `transcript.txt` (each tool call with its command and short output, the final answer, files made; checks get it as `$FIREGOAT_TRANSCRIPT`), `run.json` (status, prompt, end_goal, check results, paths), `work/`, logs. Read the transcript to judge `end_goal`; claude's full session file is ~50x larger. `summary.json` covers the whole eval; `--json` prints it.
|
|
91
110
|
|
|
92
|
-
`firegoat.yaml` keys: `runs`, `setup`, `workdir_setup`, `vars`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
111
|
+
`firegoat.yaml` keys: `runs`, `setup`, `workdir_setup`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
93
112
|
|
|
94
113
|
Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See [`examples/local-npm-registry`](examples/local-npm-registry/README.md).
|
|
95
114
|
|
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.12.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7072,11 +7072,19 @@ function parseConfig(text) {
|
|
|
7072
7072
|
}
|
|
7073
7073
|
if (!isRecord(raw))
|
|
7074
7074
|
fail("(root)", "must be a mapping");
|
|
7075
|
-
onlyKeys(raw, "(root)", [
|
|
7075
|
+
onlyKeys(raw, "(root)", [
|
|
7076
|
+
"runs",
|
|
7077
|
+
"setup",
|
|
7078
|
+
"workdir_setup",
|
|
7079
|
+
"vars",
|
|
7080
|
+
"env",
|
|
7081
|
+
"tests"
|
|
7082
|
+
]);
|
|
7076
7083
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7077
7084
|
const setup = commands(raw.setup, "setup");
|
|
7078
7085
|
const workdirSetup = commands(raw.workdir_setup, "workdir_setup");
|
|
7079
7086
|
const vars = parseVars(raw.vars);
|
|
7087
|
+
const env = parseEnv(raw.env, "env");
|
|
7080
7088
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
7081
7089
|
fail("tests", "must be a non-empty list");
|
|
7082
7090
|
const names = new Set;
|
|
@@ -7084,7 +7092,7 @@ function parseConfig(text) {
|
|
|
7084
7092
|
const p = `tests[${i}]`;
|
|
7085
7093
|
if (!isRecord(t))
|
|
7086
7094
|
fail(p, "must be a mapping");
|
|
7087
|
-
onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout"]);
|
|
7095
|
+
onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout", "env"]);
|
|
7088
7096
|
const name = str(t.name, `${p}.name`);
|
|
7089
7097
|
if (!NAME.test(name))
|
|
7090
7098
|
fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
|
|
@@ -7107,7 +7115,8 @@ function parseConfig(text) {
|
|
|
7107
7115
|
prompt: str(t.prompt, `${p}.prompt`),
|
|
7108
7116
|
end_goal: str(t.end_goal, `${p}.end_goal`),
|
|
7109
7117
|
checks,
|
|
7110
|
-
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
|
|
7118
|
+
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
7119
|
+
env: { ...env, ...parseEnv(t.env, `${p}.env`) }
|
|
7111
7120
|
};
|
|
7112
7121
|
});
|
|
7113
7122
|
return {
|
|
@@ -7115,6 +7124,7 @@ function parseConfig(text) {
|
|
|
7115
7124
|
setup,
|
|
7116
7125
|
workdirSetup,
|
|
7117
7126
|
vars,
|
|
7127
|
+
env,
|
|
7118
7128
|
tests
|
|
7119
7129
|
};
|
|
7120
7130
|
}
|
|
@@ -7144,6 +7154,54 @@ function parseVars(raw) {
|
|
|
7144
7154
|
}
|
|
7145
7155
|
return out;
|
|
7146
7156
|
}
|
|
7157
|
+
var RESERVED_ENV = new Set([
|
|
7158
|
+
"PATH",
|
|
7159
|
+
"CLAUDE_CONFIG_DIR",
|
|
7160
|
+
"CLAUDE_SECURESTORAGE_CONFIG_DIR"
|
|
7161
|
+
]);
|
|
7162
|
+
var RUN_PLACEHOLDERS = ["run.id", "run.n", "run.workdir", "test.name"];
|
|
7163
|
+
var EVAL_PLACEHOLDERS = ["run.id"];
|
|
7164
|
+
var ANY_PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g;
|
|
7165
|
+
function parseEnv(raw, path) {
|
|
7166
|
+
if (raw === undefined)
|
|
7167
|
+
return {};
|
|
7168
|
+
if (!isRecord(raw))
|
|
7169
|
+
fail(path, "must be a mapping of NAME to value");
|
|
7170
|
+
const out = {};
|
|
7171
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
7172
|
+
const p = `${path}.${k}`;
|
|
7173
|
+
if (!VAR_KEY.test(k))
|
|
7174
|
+
fail(p, "name must be letters, digits and _, not starting with a digit");
|
|
7175
|
+
if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
|
|
7176
|
+
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
|
|
7177
|
+
if (typeof v !== "string")
|
|
7178
|
+
fail(p, "must be a string (quote numbers and booleans)");
|
|
7179
|
+
for (const m of v.matchAll(ANY_PLACEHOLDER)) {
|
|
7180
|
+
const name = m[1];
|
|
7181
|
+
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !RUN_PLACEHOLDERS.includes(name))
|
|
7182
|
+
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${RUN_PLACEHOLDERS.map((n) => `{{${n}}}`).join(", ")})`);
|
|
7183
|
+
}
|
|
7184
|
+
out[k] = v;
|
|
7185
|
+
}
|
|
7186
|
+
return out;
|
|
7187
|
+
}
|
|
7188
|
+
function isEvalLevel(value) {
|
|
7189
|
+
return [...value.matchAll(ANY_PLACEHOLDER)].every((m) => {
|
|
7190
|
+
const name = m[1];
|
|
7191
|
+
return name.startsWith("vars.") || EVAL_PLACEHOLDERS.includes(name);
|
|
7192
|
+
});
|
|
7193
|
+
}
|
|
7194
|
+
function renderEnv(env, vars, run) {
|
|
7195
|
+
const out = {};
|
|
7196
|
+
for (const [k, v] of Object.entries(env))
|
|
7197
|
+
out[k] = v.replace(ANY_PLACEHOLDER, (whole, name) => {
|
|
7198
|
+
const filled = name.startsWith("vars.") ? vars[name.slice("vars.".length)] : run[name];
|
|
7199
|
+
if (filled === undefined)
|
|
7200
|
+
throw new ConfigError(`env.${k}: no value for ${whole}`);
|
|
7201
|
+
return filled;
|
|
7202
|
+
});
|
|
7203
|
+
return out;
|
|
7204
|
+
}
|
|
7147
7205
|
var PLACEHOLDER = /\{\{\s*vars\.([A-Za-z0-9_]+)\s*\}\}/g;
|
|
7148
7206
|
function varRefs(text) {
|
|
7149
7207
|
return [...text.matchAll(PLACEHOLDER)].map((m) => m[1]);
|
|
@@ -7172,7 +7230,7 @@ import {
|
|
|
7172
7230
|
readdirSync as readdirSync2,
|
|
7173
7231
|
writeFileSync as writeFileSync3
|
|
7174
7232
|
} from "node:fs";
|
|
7175
|
-
import { delimiter as delimiter2, join as join4 } from "node:path";
|
|
7233
|
+
import { basename, delimiter as delimiter2, join as join4 } from "node:path";
|
|
7176
7234
|
|
|
7177
7235
|
// src/claude.ts
|
|
7178
7236
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -7186,9 +7244,14 @@ var CLAUDE_FLAGS = [
|
|
|
7186
7244
|
"",
|
|
7187
7245
|
"--disable-slash-commands"
|
|
7188
7246
|
];
|
|
7189
|
-
|
|
7247
|
+
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7248
|
+
function hostEnv() {
|
|
7249
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k)));
|
|
7250
|
+
}
|
|
7251
|
+
function claudeEnv(configDir, env = {}) {
|
|
7190
7252
|
return {
|
|
7191
|
-
...
|
|
7253
|
+
...hostEnv(),
|
|
7254
|
+
...env,
|
|
7192
7255
|
CLAUDE_CONFIG_DIR: configDir,
|
|
7193
7256
|
CLAUDE_SECURESTORAGE_CONFIG_DIR: ""
|
|
7194
7257
|
};
|
|
@@ -7226,6 +7289,7 @@ function run(opts) {
|
|
|
7226
7289
|
}
|
|
7227
7290
|
async function runClaude(opts) {
|
|
7228
7291
|
let stdout = "";
|
|
7292
|
+
let pending = "";
|
|
7229
7293
|
const exited = await run({
|
|
7230
7294
|
cmd: opts.bin,
|
|
7231
7295
|
args: ["-p", opts.prompt, ...CLAUDE_FLAGS],
|
|
@@ -7233,7 +7297,19 @@ async function runClaude(opts) {
|
|
|
7233
7297
|
env: opts.env,
|
|
7234
7298
|
timeoutMs: opts.timeoutMs,
|
|
7235
7299
|
onStdout: (c) => {
|
|
7236
|
-
|
|
7300
|
+
const text = c.toString();
|
|
7301
|
+
stdout += text;
|
|
7302
|
+
if (!opts.onEvent)
|
|
7303
|
+
return;
|
|
7304
|
+
pending += text;
|
|
7305
|
+
const lines = pending.split(`
|
|
7306
|
+
`);
|
|
7307
|
+
pending = lines.pop() ?? "";
|
|
7308
|
+
for (const line of lines) {
|
|
7309
|
+
try {
|
|
7310
|
+
opts.onEvent(JSON.parse(line));
|
|
7311
|
+
} catch {}
|
|
7312
|
+
}
|
|
7237
7313
|
},
|
|
7238
7314
|
onStderr: (c) => opts.stderr.write(c)
|
|
7239
7315
|
});
|
|
@@ -7391,6 +7467,12 @@ firegoat.yaml reference:
|
|
|
7391
7467
|
workdir_setup: <sh> | [...] in each fresh folder before the agent starts
|
|
7392
7468
|
vars: {KEY: value} defaults for {{vars.KEY}} in prompts and checks;
|
|
7393
7469
|
--var KEY=value overrides
|
|
7470
|
+
env: {NAME: value} env vars for the agent, and for workdir_setup and
|
|
7471
|
+
checks of the same run; values are strings and can
|
|
7472
|
+
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
7473
|
+
{{run.workdir}}, {{test.name}}. setup gets only the
|
|
7474
|
+
entries that use nothing but {{vars.*}}/{{run.id}}.
|
|
7475
|
+
PATH, CLAUDE_CONFIG_DIR and FIREGOAT_* are reserved.
|
|
7394
7476
|
tests:
|
|
7395
7477
|
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
7396
7478
|
prompt: ... what a user would ask the agent; one concrete outcome
|
|
@@ -7399,6 +7481,7 @@ firegoat.yaml reference:
|
|
|
7399
7481
|
checks: optional shell commands, run in the agent's folder
|
|
7400
7482
|
- run: ... afterwards; exit 0 = pass; they set eval's exit code
|
|
7401
7483
|
timeout: 600 optional, seconds
|
|
7484
|
+
env: {NAME: value} optional, on top of the top-level env:
|
|
7402
7485
|
|
|
7403
7486
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7404
7487
|
.firegoat/fixtures/ and copy them in setup or workdir_setup. setup, workdir_setup and checks get the vars as env vars,
|
|
@@ -7407,6 +7490,11 @@ firegoat.yaml reference:
|
|
|
7407
7490
|
each output), and
|
|
7408
7491
|
$FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
7409
7492
|
|
|
7493
|
+
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
7494
|
+
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
7495
|
+
npm_command, INIT_CWD; e.g. npm_config_yes=true from npx -y) are removed from
|
|
7496
|
+
every env firegoat starts, unless env: sets them.
|
|
7497
|
+
|
|
7410
7498
|
Example:
|
|
7411
7499
|
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7412
7500
|
workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
@@ -7581,6 +7669,237 @@ ${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
|
7581
7669
|
`);
|
|
7582
7670
|
}
|
|
7583
7671
|
|
|
7672
|
+
// src/ui.ts
|
|
7673
|
+
import { relative as relative2 } from "node:path";
|
|
7674
|
+
var RAMP = "▁▂▃▄▅▆▇";
|
|
7675
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7676
|
+
var SPIN = ["◐", "◓", "◑", "◒"];
|
|
7677
|
+
function painter(color) {
|
|
7678
|
+
return (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
7679
|
+
}
|
|
7680
|
+
var C = {
|
|
7681
|
+
ember: "38;5;208",
|
|
7682
|
+
green: "32",
|
|
7683
|
+
red: "31",
|
|
7684
|
+
amber: "33",
|
|
7685
|
+
gray: "90",
|
|
7686
|
+
bold: "1",
|
|
7687
|
+
cyan: "36"
|
|
7688
|
+
};
|
|
7689
|
+
function tier(rows) {
|
|
7690
|
+
const graded = rows.filter((r) => r.state === "pass" || r.state === "fail");
|
|
7691
|
+
if (graded.length === 0)
|
|
7692
|
+
return null;
|
|
7693
|
+
const share = graded.filter((r) => r.state === "pass").length / graded.length;
|
|
7694
|
+
return share === 1 ? "S" : share >= 0.75 ? "A" : share >= 0.5 ? "B" : "C";
|
|
7695
|
+
}
|
|
7696
|
+
function notGradedWhy(reason) {
|
|
7697
|
+
const said = /claude result is an error \(subtype=[^)]*\): (.*)$/s.exec(reason);
|
|
7698
|
+
if (said)
|
|
7699
|
+
return `claude said: "${said[1]}"`;
|
|
7700
|
+
return reason.replace(/^not graded: /, "");
|
|
7701
|
+
}
|
|
7702
|
+
function ramp(r) {
|
|
7703
|
+
const blocks = Array.from({ length: Math.min(r.turns, 7) }, (_, i) => RAMP[i]).join("");
|
|
7704
|
+
const end = r.state === "pass" ? "▲" : r.state === "fail" ? "╳" : r.state === "error" ? "╌" : r.state === "running" ? "●" : "";
|
|
7705
|
+
return (blocks + end).padEnd(8);
|
|
7706
|
+
}
|
|
7707
|
+
function usage(r) {
|
|
7708
|
+
const parts = [];
|
|
7709
|
+
if (r.cost !== null && r.cost !== undefined)
|
|
7710
|
+
parts.push(`$${r.cost.toFixed(2)}`);
|
|
7711
|
+
parts.push(`${r.turns} turns`);
|
|
7712
|
+
if (r.seconds !== undefined)
|
|
7713
|
+
parts.push(`${r.seconds}s`);
|
|
7714
|
+
return parts.join(" ");
|
|
7715
|
+
}
|
|
7716
|
+
function checksPassed(r) {
|
|
7717
|
+
const all = r.checks ?? [];
|
|
7718
|
+
return `${all.filter((c) => c.code === 0).length}/${all.length} checks`;
|
|
7719
|
+
}
|
|
7720
|
+
function total(rows) {
|
|
7721
|
+
const cost = rows.reduce((s, r) => s + (r.cost ?? 0), 0);
|
|
7722
|
+
const turns = rows.reduce((s, r) => s + r.turns, 0);
|
|
7723
|
+
const seconds = rows.reduce((s, r) => s + (r.seconds ?? 0), 0);
|
|
7724
|
+
const time = seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
7725
|
+
return { cost, turns, time };
|
|
7726
|
+
}
|
|
7727
|
+
function counts(rows) {
|
|
7728
|
+
return {
|
|
7729
|
+
pass: rows.filter((r) => r.state === "pass").length,
|
|
7730
|
+
fail: rows.filter((r) => r.state === "fail").length,
|
|
7731
|
+
error: rows.filter((r) => r.state === "error").length
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
function plainUI(write) {
|
|
7735
|
+
let rows = [];
|
|
7736
|
+
const printed = new Set;
|
|
7737
|
+
return {
|
|
7738
|
+
auth: (text) => write(`auth: ${text}`),
|
|
7739
|
+
setup: (state, seconds) => {
|
|
7740
|
+
if (state === "running")
|
|
7741
|
+
write("setup...");
|
|
7742
|
+
else if (state === "fail")
|
|
7743
|
+
write(`setup failed (${seconds}s)`);
|
|
7744
|
+
},
|
|
7745
|
+
runs: (r) => {
|
|
7746
|
+
rows = r;
|
|
7747
|
+
},
|
|
7748
|
+
update: () => {
|
|
7749
|
+
for (const r of rows) {
|
|
7750
|
+
if (printed.has(r) || r.state === "queued" || r.state === "running")
|
|
7751
|
+
continue;
|
|
7752
|
+
printed.add(r);
|
|
7753
|
+
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
7754
|
+
const u = `$${(r.cost ?? 0).toFixed(2)}, ${r.turns} turns, ${r.seconds ?? 0}s`;
|
|
7755
|
+
write(`${label} ${r.test}#${r.n} ${r.reason} (${u})`);
|
|
7756
|
+
}
|
|
7757
|
+
},
|
|
7758
|
+
done: ({ runsDir, rows: all }) => {
|
|
7759
|
+
const c = counts(all);
|
|
7760
|
+
const t = total(all);
|
|
7761
|
+
const tr = tier(all);
|
|
7762
|
+
write("");
|
|
7763
|
+
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs, $${t.cost.toFixed(2)})${tr ? `, tier ${tr}` : ""}`);
|
|
7764
|
+
write(`runs: ${runsDir} (end_goal is for you to judge: read <test>/<n>/transcript.txt)`);
|
|
7765
|
+
}
|
|
7766
|
+
};
|
|
7767
|
+
}
|
|
7768
|
+
function ttyUI(out, opts) {
|
|
7769
|
+
const p = painter(opts.color);
|
|
7770
|
+
let rows = [];
|
|
7771
|
+
let setup = null;
|
|
7772
|
+
let authText = "";
|
|
7773
|
+
let frame = 0;
|
|
7774
|
+
let drawn = 0;
|
|
7775
|
+
let finished = null;
|
|
7776
|
+
const width = () => Math.min(out.columns || 80, 100);
|
|
7777
|
+
const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
7778
|
+
const statusWord = (r) => {
|
|
7779
|
+
switch (r.state) {
|
|
7780
|
+
case "pass":
|
|
7781
|
+
return p(C.green, "PASS ");
|
|
7782
|
+
case "fail":
|
|
7783
|
+
return p(C.red, "FAIL ");
|
|
7784
|
+
case "error":
|
|
7785
|
+
return p(C.amber, "NOT GRADED");
|
|
7786
|
+
case "running":
|
|
7787
|
+
return p(C.ember, "running ");
|
|
7788
|
+
default:
|
|
7789
|
+
return p(C.gray, "queued ");
|
|
7790
|
+
}
|
|
7791
|
+
};
|
|
7792
|
+
const glyph = (r) => {
|
|
7793
|
+
switch (r.state) {
|
|
7794
|
+
case "pass":
|
|
7795
|
+
return p(C.green, "●");
|
|
7796
|
+
case "fail":
|
|
7797
|
+
return p(C.red, "✗");
|
|
7798
|
+
case "error":
|
|
7799
|
+
return p(C.amber, "?");
|
|
7800
|
+
case "running":
|
|
7801
|
+
return p(C.ember, SPIN[frame % SPIN.length]);
|
|
7802
|
+
default:
|
|
7803
|
+
return p(C.gray, "○");
|
|
7804
|
+
}
|
|
7805
|
+
};
|
|
7806
|
+
const lines = () => {
|
|
7807
|
+
const w = width();
|
|
7808
|
+
const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
|
|
7809
|
+
const c = counts(rows);
|
|
7810
|
+
const doneCount = c.pass + c.fail + c.error;
|
|
7811
|
+
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
7812
|
+
const title = ` \uD83E\uDD8B firegoat eval · ${opts.project}`;
|
|
7813
|
+
const out = [
|
|
7814
|
+
`${p(C.ember, " \uD83E\uDD8B")}${title.slice(3)}${" ".repeat(Math.max(1, w - title.length - badge.length - 1))}${p(finished ? C.green : C.ember, badge)}`,
|
|
7815
|
+
rule
|
|
7816
|
+
];
|
|
7817
|
+
if (authText)
|
|
7818
|
+
out.push(p(C.gray, ` auth ${authText}`));
|
|
7819
|
+
if (setup) {
|
|
7820
|
+
const mark = setup.state === "ok" ? p(C.green, "✓") : setup.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
7821
|
+
out.push(` ${setup.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} setup${" ".repeat(Math.max(1, w - 20))}${mark}${setup.seconds !== undefined ? ` ${setup.seconds}s` : ""}`);
|
|
7822
|
+
}
|
|
7823
|
+
rows.forEach((r, i) => {
|
|
7824
|
+
out.push(p(C.gray, " │"));
|
|
7825
|
+
const num = String(i + 1).padStart(2, "0");
|
|
7826
|
+
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
7827
|
+
let detail = "";
|
|
7828
|
+
if (r.state === "running")
|
|
7829
|
+
detail = p(C.gray, `turn ${r.turns}`);
|
|
7830
|
+
else if (r.state === "pass" || r.state === "fail")
|
|
7831
|
+
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
|
|
7832
|
+
else if (r.state === "error")
|
|
7833
|
+
detail = p(C.gray, usage(r));
|
|
7834
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
7835
|
+
if (r.state === "running" && r.command)
|
|
7836
|
+
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
7837
|
+
if (r.state === "fail")
|
|
7838
|
+
for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
|
|
7839
|
+
out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
|
|
7840
|
+
out.push(p(C.gray, ` │ ${ck.error ?? `exit ${ck.code}`}`));
|
|
7841
|
+
}
|
|
7842
|
+
if (r.state === "error")
|
|
7843
|
+
out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
|
|
7844
|
+
});
|
|
7845
|
+
out.push(rule);
|
|
7846
|
+
const t = total(rows);
|
|
7847
|
+
if (!finished) {
|
|
7848
|
+
const track = Math.max(10, w - 34);
|
|
7849
|
+
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
7850
|
+
const goat = `${"·".repeat(at)}\uD83D\uDC10${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
7851
|
+
out.push(` ${p(C.ember, goat)} ${p(C.gray, `${doneCount}/${rows.length} · $${t.cost.toFixed(2)} so far`)}`);
|
|
7852
|
+
return out;
|
|
7853
|
+
}
|
|
7854
|
+
const tr = tier(rows);
|
|
7855
|
+
const goated = c.pass === rows.length && rows.length > 0;
|
|
7856
|
+
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
7857
|
+
const right = goated ? p(C.ember, `\uD83D\uDC10 GOATED · tier ${tr}`) : tr ? `tier ${tr}` : "";
|
|
7858
|
+
out.push(` ${goated ? p(C.green, `${rows.length}/${rows.length} runs passed their checks`) : tally}${" ".repeat(Math.max(2, w - tally.length - 22))}${right}`);
|
|
7859
|
+
out.push(p(C.gray, ` total $${t.cost.toFixed(2)} · ${t.turns} turns · ${t.time}`));
|
|
7860
|
+
out.push(p(C.gray, ` runs ${relative2(process.cwd(), finished.runsDir)}/`));
|
|
7861
|
+
out.push(` checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
7862
|
+
return out;
|
|
7863
|
+
};
|
|
7864
|
+
const rowsOf = (line) => {
|
|
7865
|
+
const visible = line.replace(ANSI, "").replace(/🐐|🦋/g, "gg");
|
|
7866
|
+
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
7867
|
+
};
|
|
7868
|
+
const render = () => {
|
|
7869
|
+
const text = lines();
|
|
7870
|
+
let s = drawn > 0 ? `\x1B[${drawn}A\x1B[J` : "";
|
|
7871
|
+
s += `${text.join(`
|
|
7872
|
+
`)}
|
|
7873
|
+
`;
|
|
7874
|
+
out.write(s);
|
|
7875
|
+
drawn = text.reduce((n, l) => n + rowsOf(l), 0);
|
|
7876
|
+
};
|
|
7877
|
+
const timer = setInterval(() => {
|
|
7878
|
+
frame++;
|
|
7879
|
+
render();
|
|
7880
|
+
}, 120);
|
|
7881
|
+
timer.unref();
|
|
7882
|
+
return {
|
|
7883
|
+
auth: (text) => {
|
|
7884
|
+
authText = text;
|
|
7885
|
+
},
|
|
7886
|
+
setup: (state, seconds) => {
|
|
7887
|
+
setup = { state, seconds };
|
|
7888
|
+
render();
|
|
7889
|
+
},
|
|
7890
|
+
runs: (r) => {
|
|
7891
|
+
rows = r;
|
|
7892
|
+
render();
|
|
7893
|
+
},
|
|
7894
|
+
update: render,
|
|
7895
|
+
done: ({ runId, runsDir }) => {
|
|
7896
|
+
clearInterval(timer);
|
|
7897
|
+
finished = { runId, runsDir };
|
|
7898
|
+
render();
|
|
7899
|
+
}
|
|
7900
|
+
};
|
|
7901
|
+
}
|
|
7902
|
+
|
|
7584
7903
|
// src/which.ts
|
|
7585
7904
|
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
7586
7905
|
import { delimiter, join as join3, resolve } from "node:path";
|
|
@@ -7662,15 +7981,6 @@ function findSession(configDir, sessionId) {
|
|
|
7662
7981
|
}
|
|
7663
7982
|
return null;
|
|
7664
7983
|
}
|
|
7665
|
-
function usage(r) {
|
|
7666
|
-
const parts = [];
|
|
7667
|
-
if (r.cost !== null)
|
|
7668
|
-
parts.push(`$${r.cost.toFixed(2)}`);
|
|
7669
|
-
if (r.num_turns !== null)
|
|
7670
|
-
parts.push(`${r.num_turns} turns`);
|
|
7671
|
-
parts.push(`${Math.round(r.duration_ms / 1000)}s`);
|
|
7672
|
-
return parts.join(", ");
|
|
7673
|
-
}
|
|
7674
7984
|
function newRunId() {
|
|
7675
7985
|
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
7676
7986
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
@@ -7707,7 +8017,13 @@ async function runOne(opts) {
|
|
|
7707
8017
|
`);
|
|
7708
8018
|
return r;
|
|
7709
8019
|
};
|
|
7710
|
-
const
|
|
8020
|
+
const env = renderEnv(test.env, opts.vars, {
|
|
8021
|
+
"run.id": opts.runId,
|
|
8022
|
+
"run.n": String(n),
|
|
8023
|
+
"run.workdir": workdir,
|
|
8024
|
+
"test.name": test.name
|
|
8025
|
+
});
|
|
8026
|
+
const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
|
|
7711
8027
|
if (opts.workdirSetup.length > 0) {
|
|
7712
8028
|
const error = await runSteps({
|
|
7713
8029
|
what: "workdir_setup",
|
|
@@ -7724,9 +8040,10 @@ async function runOne(opts) {
|
|
|
7724
8040
|
bin: opts.claudeBin,
|
|
7725
8041
|
prompt: r.prompt,
|
|
7726
8042
|
cwd: workdir,
|
|
7727
|
-
env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
|
|
8043
|
+
env: { ...claudeEnv(opts.configDir, env), PATH: opts.stepEnv.PATH },
|
|
7728
8044
|
stderr,
|
|
7729
|
-
timeoutMs: test.timeout * 1000
|
|
8045
|
+
timeoutMs: test.timeout * 1000,
|
|
8046
|
+
onEvent: opts.onEvent
|
|
7730
8047
|
});
|
|
7731
8048
|
await new Promise((res) => stderr.end(res));
|
|
7732
8049
|
if (outcome.sessionId)
|
|
@@ -7787,6 +8104,8 @@ async function evalCommand(args, root) {
|
|
|
7787
8104
|
throw e;
|
|
7788
8105
|
}
|
|
7789
8106
|
const say = opts.json ? console.error : console.log;
|
|
8107
|
+
const stream = opts.json ? process.stderr : process.stdout;
|
|
8108
|
+
const ui = stream.isTTY ? ttyUI(stream, { project: basename(root), color: !process.env.NO_COLOR }) : plainUI(say);
|
|
7790
8109
|
let config;
|
|
7791
8110
|
try {
|
|
7792
8111
|
config = loadConfig(root);
|
|
@@ -7806,7 +8125,11 @@ async function evalCommand(args, root) {
|
|
|
7806
8125
|
const runs = opts.runs ?? config.runs;
|
|
7807
8126
|
const vars = { ...config.vars, ...opts.vars };
|
|
7808
8127
|
const missing = [
|
|
7809
|
-
...new Set(tests.flatMap((t) => [
|
|
8128
|
+
...new Set(tests.flatMap((t) => [
|
|
8129
|
+
t.prompt,
|
|
8130
|
+
...t.checks.map((c) => c.run),
|
|
8131
|
+
...Object.values(t.env)
|
|
8132
|
+
]).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
7810
8133
|
];
|
|
7811
8134
|
if (missing.length > 0)
|
|
7812
8135
|
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`);
|
|
@@ -7816,7 +8139,7 @@ async function evalCommand(args, root) {
|
|
|
7816
8139
|
const firegoatDir = ensureFiregoatDir(root);
|
|
7817
8140
|
const configDir = join4(firegoatDir, ".claude");
|
|
7818
8141
|
mkdirSync2(configDir, { recursive: true });
|
|
7819
|
-
|
|
8142
|
+
ui.auth(process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login");
|
|
7820
8143
|
const loginProblem = checkLogin({
|
|
7821
8144
|
bin: claudePath,
|
|
7822
8145
|
env: claudeEnv(configDir),
|
|
@@ -7829,29 +8152,60 @@ async function evalCommand(args, root) {
|
|
|
7829
8152
|
const out = join4(runDir, "out");
|
|
7830
8153
|
mkdirSync2(out, { recursive: true });
|
|
7831
8154
|
const stepEnv = {
|
|
7832
|
-
...
|
|
8155
|
+
...hostEnv(),
|
|
7833
8156
|
...vars,
|
|
7834
8157
|
FIREGOAT_OUT: out,
|
|
7835
8158
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
7836
8159
|
};
|
|
8160
|
+
const rows = tests.flatMap((t) => Array.from({ length: runs }, (_, i) => ({
|
|
8161
|
+
test: t.name,
|
|
8162
|
+
n: i + 1,
|
|
8163
|
+
state: "queued",
|
|
8164
|
+
turns: 0
|
|
8165
|
+
})));
|
|
7837
8166
|
if (config.setup.length > 0) {
|
|
7838
|
-
|
|
8167
|
+
const setupStarted = Date.now();
|
|
8168
|
+
ui.setup("running");
|
|
7839
8169
|
const error = await runSteps({
|
|
7840
8170
|
what: "setup",
|
|
7841
8171
|
commands: config.setup,
|
|
7842
8172
|
cwd: root,
|
|
7843
|
-
env:
|
|
8173
|
+
env: {
|
|
8174
|
+
...stepEnv,
|
|
8175
|
+
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId })
|
|
8176
|
+
},
|
|
7844
8177
|
logPath: join4(runDir, "setup.txt")
|
|
7845
8178
|
});
|
|
8179
|
+
const secs = Math.round((Date.now() - setupStarted) / 1000);
|
|
8180
|
+
ui.setup(error ? "fail" : "ok", secs);
|
|
7846
8181
|
if (error)
|
|
7847
8182
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
7848
8183
|
}
|
|
8184
|
+
ui.runs(rows);
|
|
7849
8185
|
const results = [];
|
|
7850
8186
|
for (const test of tests)
|
|
7851
8187
|
for (let n = 1;n <= runs; n++) {
|
|
8188
|
+
const row = rows.find((x) => x.test === test.name && x.n === n);
|
|
8189
|
+
row.state = "running";
|
|
8190
|
+
ui.update();
|
|
7852
8191
|
const r = await runOne({
|
|
8192
|
+
onEvent: (e) => {
|
|
8193
|
+
if (e.type !== "assistant")
|
|
8194
|
+
return;
|
|
8195
|
+
row.turns++;
|
|
8196
|
+
const content = e.message?.content;
|
|
8197
|
+
if (!Array.isArray(content))
|
|
8198
|
+
return;
|
|
8199
|
+
for (const b of content) {
|
|
8200
|
+
const cmd = b.input?.command;
|
|
8201
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
8202
|
+
row.command = cmd.split(`
|
|
8203
|
+
`)[0];
|
|
8204
|
+
}
|
|
8205
|
+
},
|
|
7853
8206
|
test,
|
|
7854
8207
|
n,
|
|
8208
|
+
runId,
|
|
7855
8209
|
runDir,
|
|
7856
8210
|
claudeBin: claudePath,
|
|
7857
8211
|
configDir,
|
|
@@ -7860,7 +8214,16 @@ async function evalCommand(args, root) {
|
|
|
7860
8214
|
vars
|
|
7861
8215
|
});
|
|
7862
8216
|
results.push(r);
|
|
7863
|
-
|
|
8217
|
+
Object.assign(row, {
|
|
8218
|
+
state: r.status,
|
|
8219
|
+
turns: r.num_turns ?? row.turns,
|
|
8220
|
+
reason: r.reason,
|
|
8221
|
+
checks: r.checks,
|
|
8222
|
+
cost: r.cost,
|
|
8223
|
+
seconds: Math.round(r.duration_ms / 1000),
|
|
8224
|
+
command: undefined
|
|
8225
|
+
});
|
|
8226
|
+
ui.update();
|
|
7864
8227
|
}
|
|
7865
8228
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
7866
8229
|
for (const r of results)
|
|
@@ -7868,10 +8231,7 @@ async function evalCommand(args, root) {
|
|
|
7868
8231
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
7869
8232
|
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
7870
8233
|
`);
|
|
7871
|
-
|
|
7872
|
-
say(`
|
|
7873
|
-
${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${results.length} runs, $${cost.toFixed(2)})`);
|
|
7874
|
-
say(`runs: ${runDir} (end_goal is for you to judge: see run.json per run)`);
|
|
8234
|
+
ui.done({ runId, runsDir: runDir, rows });
|
|
7875
8235
|
if (opts.json)
|
|
7876
8236
|
console.log(JSON.stringify(doc, null, 2));
|
|
7877
8237
|
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
@@ -7880,7 +8240,7 @@ ${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${resu
|
|
|
7880
8240
|
// src/init.ts
|
|
7881
8241
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7882
8242
|
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7883
|
-
import { basename, join as join5 } from "node:path";
|
|
8243
|
+
import { basename as basename2, join as join5 } from "node:path";
|
|
7884
8244
|
import { createInterface } from "node:readline/promises";
|
|
7885
8245
|
var SKILLS_ARGV = [
|
|
7886
8246
|
"skills",
|
|
@@ -7924,6 +8284,12 @@ runs: 1
|
|
|
7924
8284
|
# setup runs in this folder, so: setup: cp -R .firegoat/fixtures/. "$FIREGOAT_OUT"
|
|
7925
8285
|
# then e.g. workdir_setup: cp "$FIREGOAT_OUT/wizard.mjs" .
|
|
7926
8286
|
|
|
8287
|
+
# Optional: env vars for the agent (and for workdir_setup and checks of the same
|
|
8288
|
+
# run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
|
|
8289
|
+
# {{run.id}}, {{run.n}}, {{run.workdir}}, {{test.name}}. A test can add its own env:.
|
|
8290
|
+
# env:
|
|
8291
|
+
# MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{run.n}}"
|
|
8292
|
+
|
|
7927
8293
|
tests:
|
|
7928
8294
|
- name: quickstart
|
|
7929
8295
|
# What a user would ask their agent. Edit this.
|
|
@@ -7983,48 +8349,73 @@ ${INIT_HELP}`);
|
|
|
7983
8349
|
return EXIT.usage;
|
|
7984
8350
|
}
|
|
7985
8351
|
}
|
|
8352
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8353
|
+
const color = interactive && !process.env.NO_COLOR;
|
|
8354
|
+
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
8355
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
8356
|
+
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
8357
|
+
const step = (n, label, status) => console.log(` ${paint("32", "●")} ${paint("90", String(n).padStart(2, "0"))} ${label.padEnd(28)}${paint("90", status)}`);
|
|
8358
|
+
if (interactive) {
|
|
8359
|
+
const title = " \uD83E\uDD8B firegoat init";
|
|
8360
|
+
const tag = "Get agent-ready.";
|
|
8361
|
+
console.log(`${paint("38;5;208", " \uD83E\uDD8B")} firegoat init${" ".repeat(Math.max(2, width - title.length - tag.length - 2))}${paint("90", tag)}`);
|
|
8362
|
+
console.log(rule);
|
|
8363
|
+
}
|
|
7986
8364
|
ensureFiregoatDir(cwd);
|
|
7987
8365
|
const configPath = join5(cwd, CONFIG_FILE);
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
8366
|
+
const keep = existsSync3(configPath) && !force;
|
|
8367
|
+
if (!keep)
|
|
8368
|
+
writeFileSync4(configPath, template(basename2(cwd)));
|
|
8369
|
+
if (interactive) {
|
|
8370
|
+
step(1, CONFIG_FILE, keep ? "kept (--force replaces it)" : "created");
|
|
8371
|
+
console.log(paint("90", " │"));
|
|
8372
|
+
step(2, ".firegoat/", "ready");
|
|
8373
|
+
} else
|
|
8374
|
+
console.log(keep ? `kept ${CONFIG_FILE} (--force replaces it)` : `wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
8375
|
+
if (skills && interactive) {
|
|
8376
|
+
console.log(paint("90", " │"));
|
|
8377
|
+
skills = await askYes(` ${paint("38;5;208", "◉")} ${paint("90", "03")} Install the firegoat-onboarding skill?`);
|
|
8378
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
8379
|
+
}
|
|
7997
8380
|
if (skills) {
|
|
7998
|
-
|
|
8381
|
+
if (!interactive)
|
|
8382
|
+
console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
|
|
7999
8383
|
const env = { ...process.env };
|
|
8000
8384
|
delete env.npm_config_package;
|
|
8001
8385
|
const r = spawnSync2("npx", ["-y", ...SKILLS_ARGV], {
|
|
8002
8386
|
cwd,
|
|
8003
8387
|
env,
|
|
8004
|
-
stdio:
|
|
8388
|
+
stdio: "pipe",
|
|
8005
8389
|
encoding: "utf8"
|
|
8006
8390
|
});
|
|
8007
8391
|
if (r.error || r.status !== 0) {
|
|
8008
|
-
|
|
8009
|
-
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
8392
|
+
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
8010
8393
|
console.error(`firegoat init: installing the skills failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8011
8394
|
return EXIT.notGraded;
|
|
8012
8395
|
}
|
|
8013
|
-
|
|
8396
|
+
if (interactive)
|
|
8397
|
+
step(3, "firegoat-onboarding skill", "installed → .claude/skills/");
|
|
8398
|
+
else
|
|
8399
|
+
console.log("installed skills firegoat-onboarding, suggesting-eval-prompts in .claude/skills/");
|
|
8014
8400
|
const ignored = spawnSync2("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
8015
8401
|
cwd
|
|
8016
8402
|
});
|
|
8017
8403
|
if (ignored.status === 0)
|
|
8018
8404
|
console.error("warning: git ignores .claude/skills, so teammates and CI won't get the skills. Commit them with: git add -f .claude/skills");
|
|
8019
|
-
}
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
${AGENT_PROMPT}
|
|
8024
|
-
` : `
|
|
8405
|
+
} else if (interactive)
|
|
8406
|
+
step(3, "firegoat-onboarding skill", "skipped");
|
|
8407
|
+
if (!interactive) {
|
|
8408
|
+
console.log(`
|
|
8025
8409
|
Prompt for your coding agent: ${AGENT_PROMPT}`);
|
|
8026
|
-
|
|
8027
|
-
|
|
8410
|
+
return EXIT.pass;
|
|
8411
|
+
}
|
|
8412
|
+
console.log(rule);
|
|
8413
|
+
console.log(` next: hand this to your coding agent
|
|
8414
|
+
`);
|
|
8415
|
+
console.log(AGENT_PROMPT);
|
|
8416
|
+
console.log("");
|
|
8417
|
+
const copied = copyToClipboard(AGENT_PROMPT);
|
|
8418
|
+
console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}then: ${paint("36", "npx firegoat eval")}`);
|
|
8028
8419
|
return EXIT.pass;
|
|
8029
8420
|
}
|
|
8030
8421
|
|