firegoat 0.9.4 → 0.11.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 +4 -4
- package/dist/cli.js +485 -75
- 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
|
|
|
@@ -30,7 +30,7 @@ tests:
|
|
|
30
30
|
and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
|
|
31
31
|
end_goal: A script sends one event with the SDK's client and prints its id.
|
|
32
32
|
checks:
|
|
33
|
-
- run: grep -qE 'evt_[A-Za-z0-9]+' "$
|
|
33
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`setup` builds your package once; `workdir_setup` installs that build into each fresh folder, so the agent uses your local code, not the registry.
|
|
@@ -44,7 +44,7 @@ tests:
|
|
|
44
44
|
prompt: Deploy the hello-world template to a preview environment with the acme CLI and print the URL.
|
|
45
45
|
end_goal: The agent finds the right acme commands and ends with a working preview URL.
|
|
46
46
|
checks:
|
|
47
|
-
- run: grep -q '
|
|
47
|
+
- run: grep -q '\$ acme deploy' "$FIREGOAT_TRANSCRIPT"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Whatever `setup` writes to `$FIREGOAT_OUT` is first on the agent's `PATH`. That only helps for binaries called by bare name: if users run your CLI via `npx`/`uvx`, pack and install it like the npm example, or `npx` fetches the registry release.
|
|
@@ -87,7 +87,7 @@ npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
|
87
87
|
- 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
88
|
- 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`.
|
|
89
89
|
- 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
|
-
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `run.json` (status, prompt, end_goal, check results, paths), `work/`, logs. `summary.json` covers the whole eval; `--json` prints it.
|
|
90
|
+
- 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
91
|
|
|
92
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`.
|
|
93
93
|
|
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.11.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7169,10 +7169,10 @@ import {
|
|
|
7169
7169
|
createWriteStream as createWriteStream2,
|
|
7170
7170
|
existsSync as existsSync2,
|
|
7171
7171
|
mkdirSync as mkdirSync2,
|
|
7172
|
-
readdirSync,
|
|
7173
|
-
writeFileSync as
|
|
7172
|
+
readdirSync as readdirSync2,
|
|
7173
|
+
writeFileSync as writeFileSync3
|
|
7174
7174
|
} from "node:fs";
|
|
7175
|
-
import { delimiter as delimiter2, join as
|
|
7175
|
+
import { basename, delimiter as delimiter2, join as join4 } from "node:path";
|
|
7176
7176
|
|
|
7177
7177
|
// src/claude.ts
|
|
7178
7178
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -7226,6 +7226,7 @@ function run(opts) {
|
|
|
7226
7226
|
}
|
|
7227
7227
|
async function runClaude(opts) {
|
|
7228
7228
|
let stdout = "";
|
|
7229
|
+
let pending = "";
|
|
7229
7230
|
const exited = await run({
|
|
7230
7231
|
cmd: opts.bin,
|
|
7231
7232
|
args: ["-p", opts.prompt, ...CLAUDE_FLAGS],
|
|
@@ -7233,12 +7234,25 @@ async function runClaude(opts) {
|
|
|
7233
7234
|
env: opts.env,
|
|
7234
7235
|
timeoutMs: opts.timeoutMs,
|
|
7235
7236
|
onStdout: (c) => {
|
|
7236
|
-
|
|
7237
|
+
const text = c.toString();
|
|
7238
|
+
stdout += text;
|
|
7239
|
+
if (!opts.onEvent)
|
|
7240
|
+
return;
|
|
7241
|
+
pending += text;
|
|
7242
|
+
const lines = pending.split(`
|
|
7243
|
+
`);
|
|
7244
|
+
pending = lines.pop() ?? "";
|
|
7245
|
+
for (const line of lines) {
|
|
7246
|
+
try {
|
|
7247
|
+
opts.onEvent(JSON.parse(line));
|
|
7248
|
+
} catch {}
|
|
7249
|
+
}
|
|
7237
7250
|
},
|
|
7238
7251
|
onStderr: (c) => opts.stderr.write(c)
|
|
7239
7252
|
});
|
|
7240
7253
|
if (exited.error)
|
|
7241
7254
|
return {
|
|
7255
|
+
events: [],
|
|
7242
7256
|
ok: false,
|
|
7243
7257
|
reason: `could not start ${opts.bin}: ${exited.error.message}`
|
|
7244
7258
|
};
|
|
@@ -7255,6 +7269,7 @@ async function runClaude(opts) {
|
|
|
7255
7269
|
const said = stdout.trim().split(`
|
|
7256
7270
|
`)[0]?.slice(0, 300);
|
|
7257
7271
|
return {
|
|
7272
|
+
events,
|
|
7258
7273
|
ok: false,
|
|
7259
7274
|
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7260
7275
|
};
|
|
@@ -7263,6 +7278,7 @@ async function runClaude(opts) {
|
|
|
7263
7278
|
const result = events.findLast((e) => e?.type === "result");
|
|
7264
7279
|
if (exited.timedOut)
|
|
7265
7280
|
return {
|
|
7281
|
+
events,
|
|
7266
7282
|
ok: false,
|
|
7267
7283
|
reason: `timed out after ${opts.timeoutMs / 1000}s`,
|
|
7268
7284
|
result,
|
|
@@ -7270,18 +7286,20 @@ async function runClaude(opts) {
|
|
|
7270
7286
|
};
|
|
7271
7287
|
if (!result)
|
|
7272
7288
|
return {
|
|
7289
|
+
events,
|
|
7273
7290
|
ok: false,
|
|
7274
7291
|
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7275
7292
|
sessionId
|
|
7276
7293
|
};
|
|
7277
7294
|
if (result.is_error === true || result.subtype !== "success")
|
|
7278
7295
|
return {
|
|
7296
|
+
events,
|
|
7279
7297
|
ok: false,
|
|
7280
7298
|
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7281
7299
|
result,
|
|
7282
7300
|
sessionId
|
|
7283
7301
|
};
|
|
7284
|
-
return { ok: true, result, sessionId };
|
|
7302
|
+
return { events, ok: true, result, sessionId };
|
|
7285
7303
|
}
|
|
7286
7304
|
async function runShell(opts) {
|
|
7287
7305
|
opts.log.write(`$ ${opts.run}
|
|
@@ -7397,8 +7415,10 @@ firegoat.yaml reference:
|
|
|
7397
7415
|
|
|
7398
7416
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7399
7417
|
.firegoat/fixtures/ and copy them in setup or workdir_setup. setup, workdir_setup and checks get the vars as env vars,
|
|
7400
|
-
plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR
|
|
7401
|
-
|
|
7418
|
+
plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR. Checks also get $FIREGOAT_TRANSCRIPT, a short
|
|
7419
|
+
plain-text transcript ("[1] $ <command>" per shell call, "[2] Write <path>", then
|
|
7420
|
+
each output), and
|
|
7421
|
+
$FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
7402
7422
|
|
|
7403
7423
|
Example:
|
|
7404
7424
|
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -7412,9 +7432,9 @@ Example:
|
|
|
7412
7432
|
A script sends one event with the SDK's client (not raw HTTP) and prints its
|
|
7413
7433
|
id, without installing acme from the registry.
|
|
7414
7434
|
checks:
|
|
7415
|
-
- run: grep -qE 'evt_[A-Za-z0-9]+' "$
|
|
7435
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
7416
7436
|
- run: |
|
|
7417
|
-
! grep -Eq 'npm (install|i)
|
|
7437
|
+
! grep -Eq '$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7418
7438
|
|
|
7419
7439
|
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7420
7440
|
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json]
|
|
@@ -7440,12 +7460,15 @@ Output: .firegoat/runs/<runId>/
|
|
|
7440
7460
|
summary.json every run, plus totals
|
|
7441
7461
|
<test>/<n>/run.json one run: status and reason (from the checks), prompt,
|
|
7442
7462
|
end_goal, checks [{run, code}], and paths: workdir,
|
|
7443
|
-
session (claude's session file), stderr,
|
|
7463
|
+
transcript, session (claude's session file), stderr,
|
|
7464
|
+
checksLog
|
|
7465
|
+
<test>/<n>/transcript.txt what the agent did: each tool call with its command,
|
|
7466
|
+
short output, its final answer, files it made
|
|
7444
7467
|
setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
|
|
7445
7468
|
|
|
7446
7469
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
7447
|
-
yours to judge
|
|
7448
|
-
|
|
7470
|
+
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
7471
|
+
(often 50x larger).
|
|
7449
7472
|
|
|
7450
7473
|
Examples:
|
|
7451
7474
|
npx firegoat eval --json all tests, JSON summary on stdout
|
|
@@ -7479,14 +7502,337 @@ async function runSteps(opts) {
|
|
|
7479
7502
|
}
|
|
7480
7503
|
}
|
|
7481
7504
|
|
|
7505
|
+
// src/transcript.ts
|
|
7506
|
+
import { readdirSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7507
|
+
import { join as join2, relative } from "node:path";
|
|
7508
|
+
var OUTPUT_LINES = 15;
|
|
7509
|
+
var OUTPUT_CHARS = 1500;
|
|
7510
|
+
var TEXT_CHARS = 1500;
|
|
7511
|
+
var SKIP_DIRS = new Set(["node_modules", ".git", ".venv", "__pycache__"]);
|
|
7512
|
+
function truncate(text, maxLines, maxChars) {
|
|
7513
|
+
let out = text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
7514
|
+
const lines = out.split(`
|
|
7515
|
+
`);
|
|
7516
|
+
if (lines.length > maxLines)
|
|
7517
|
+
out = lines.slice(0, maxLines).join(`
|
|
7518
|
+
`);
|
|
7519
|
+
return out.length < text.length ? `${out}
|
|
7520
|
+
… (${text.length - out.length} more chars)` : out;
|
|
7521
|
+
}
|
|
7522
|
+
function indent(text) {
|
|
7523
|
+
return text.split(`
|
|
7524
|
+
`).map((l) => ` ${l}`).join(`
|
|
7525
|
+
`);
|
|
7526
|
+
}
|
|
7527
|
+
function describeCall(name, input) {
|
|
7528
|
+
if (typeof input.command === "string")
|
|
7529
|
+
return `$ ${input.command}`;
|
|
7530
|
+
for (const key of ["file_path", "path", "url", "pattern", "query"])
|
|
7531
|
+
if (typeof input[key] === "string")
|
|
7532
|
+
return `${name} ${input[key]}`;
|
|
7533
|
+
return `${name} ${truncate(JSON.stringify(input), 1, 300)}`;
|
|
7534
|
+
}
|
|
7535
|
+
function resultText(content) {
|
|
7536
|
+
if (typeof content === "string")
|
|
7537
|
+
return content;
|
|
7538
|
+
if (Array.isArray(content))
|
|
7539
|
+
return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
|
|
7540
|
+
`);
|
|
7541
|
+
return "";
|
|
7542
|
+
}
|
|
7543
|
+
function workdirFiles(dir) {
|
|
7544
|
+
const out = [];
|
|
7545
|
+
const walk = (d) => {
|
|
7546
|
+
for (const name of readdirSync(d)) {
|
|
7547
|
+
if (SKIP_DIRS.has(name))
|
|
7548
|
+
continue;
|
|
7549
|
+
const p = join2(d, name);
|
|
7550
|
+
if (statSync(p).isDirectory())
|
|
7551
|
+
walk(p);
|
|
7552
|
+
else
|
|
7553
|
+
out.push(relative(dir, p));
|
|
7554
|
+
}
|
|
7555
|
+
};
|
|
7556
|
+
walk(dir);
|
|
7557
|
+
return out.sort();
|
|
7558
|
+
}
|
|
7559
|
+
function writeTranscript(opts) {
|
|
7560
|
+
const out = [`# ${opts.title}`, "", "## prompt", opts.prompt, "", "## steps"];
|
|
7561
|
+
let step = 0;
|
|
7562
|
+
let final = "";
|
|
7563
|
+
let lastText = -1;
|
|
7564
|
+
for (const e of opts.events) {
|
|
7565
|
+
const message = e?.message;
|
|
7566
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
7567
|
+
if (e?.type === "assistant")
|
|
7568
|
+
for (const b of content) {
|
|
7569
|
+
if (b.type === "tool_use")
|
|
7570
|
+
out.push(`[${++step}] ${describeCall(String(b.name), b.input ?? {})}`);
|
|
7571
|
+
else if (b.type === "text" && typeof b.text === "string") {
|
|
7572
|
+
lastText = out.length;
|
|
7573
|
+
out.push(`agent: ${truncate(b.text, 30, TEXT_CHARS)}`);
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
else if (e?.type === "user")
|
|
7577
|
+
for (const b of content) {
|
|
7578
|
+
if (b.type !== "tool_result")
|
|
7579
|
+
continue;
|
|
7580
|
+
const text = resultText(b.content).trimEnd();
|
|
7581
|
+
const status = b.is_error === true ? "error" : "ok";
|
|
7582
|
+
out.push(text ? ` → ${status}
|
|
7583
|
+
${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
7584
|
+
}
|
|
7585
|
+
else if (e?.type === "result" && typeof e.result === "string")
|
|
7586
|
+
final = e.result;
|
|
7587
|
+
}
|
|
7588
|
+
if (lastText === out.length - 1 && final && out[lastText] === `agent: ${truncate(final, 30, TEXT_CHARS)}`)
|
|
7589
|
+
out.pop();
|
|
7590
|
+
out.push("", "## final answer", final || "(none)");
|
|
7591
|
+
out.push("", "## files in the workdir", ...workdirFiles(opts.workdir));
|
|
7592
|
+
writeFileSync2(opts.path, `${out.join(`
|
|
7593
|
+
`)}
|
|
7594
|
+
`);
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7597
|
+
// src/ui.ts
|
|
7598
|
+
import { relative as relative2 } from "node:path";
|
|
7599
|
+
var RAMP = "▁▂▃▄▅▆▇";
|
|
7600
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7601
|
+
var SPIN = ["◐", "◓", "◑", "◒"];
|
|
7602
|
+
function painter(color) {
|
|
7603
|
+
return (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
7604
|
+
}
|
|
7605
|
+
var C = {
|
|
7606
|
+
ember: "38;5;208",
|
|
7607
|
+
green: "32",
|
|
7608
|
+
red: "31",
|
|
7609
|
+
amber: "33",
|
|
7610
|
+
gray: "90",
|
|
7611
|
+
bold: "1",
|
|
7612
|
+
cyan: "36"
|
|
7613
|
+
};
|
|
7614
|
+
function tier(rows) {
|
|
7615
|
+
const graded = rows.filter((r) => r.state === "pass" || r.state === "fail");
|
|
7616
|
+
if (graded.length === 0)
|
|
7617
|
+
return null;
|
|
7618
|
+
const share = graded.filter((r) => r.state === "pass").length / graded.length;
|
|
7619
|
+
return share === 1 ? "S" : share >= 0.75 ? "A" : share >= 0.5 ? "B" : "C";
|
|
7620
|
+
}
|
|
7621
|
+
function notGradedWhy(reason) {
|
|
7622
|
+
const said = /claude result is an error \(subtype=[^)]*\): (.*)$/s.exec(reason);
|
|
7623
|
+
if (said)
|
|
7624
|
+
return `claude said: "${said[1]}"`;
|
|
7625
|
+
return reason.replace(/^not graded: /, "");
|
|
7626
|
+
}
|
|
7627
|
+
function ramp(r) {
|
|
7628
|
+
const blocks = Array.from({ length: Math.min(r.turns, 7) }, (_, i) => RAMP[i]).join("");
|
|
7629
|
+
const end = r.state === "pass" ? "▲" : r.state === "fail" ? "╳" : r.state === "error" ? "╌" : r.state === "running" ? "●" : "";
|
|
7630
|
+
return (blocks + end).padEnd(8);
|
|
7631
|
+
}
|
|
7632
|
+
function usage(r) {
|
|
7633
|
+
const parts = [];
|
|
7634
|
+
if (r.cost !== null && r.cost !== undefined)
|
|
7635
|
+
parts.push(`$${r.cost.toFixed(2)}`);
|
|
7636
|
+
parts.push(`${r.turns} turns`);
|
|
7637
|
+
if (r.seconds !== undefined)
|
|
7638
|
+
parts.push(`${r.seconds}s`);
|
|
7639
|
+
return parts.join(" ");
|
|
7640
|
+
}
|
|
7641
|
+
function checksPassed(r) {
|
|
7642
|
+
const all = r.checks ?? [];
|
|
7643
|
+
return `${all.filter((c) => c.code === 0).length}/${all.length} checks`;
|
|
7644
|
+
}
|
|
7645
|
+
function total(rows) {
|
|
7646
|
+
const cost = rows.reduce((s, r) => s + (r.cost ?? 0), 0);
|
|
7647
|
+
const turns = rows.reduce((s, r) => s + r.turns, 0);
|
|
7648
|
+
const seconds = rows.reduce((s, r) => s + (r.seconds ?? 0), 0);
|
|
7649
|
+
const time = seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
7650
|
+
return { cost, turns, time };
|
|
7651
|
+
}
|
|
7652
|
+
function counts(rows) {
|
|
7653
|
+
return {
|
|
7654
|
+
pass: rows.filter((r) => r.state === "pass").length,
|
|
7655
|
+
fail: rows.filter((r) => r.state === "fail").length,
|
|
7656
|
+
error: rows.filter((r) => r.state === "error").length
|
|
7657
|
+
};
|
|
7658
|
+
}
|
|
7659
|
+
function plainUI(write) {
|
|
7660
|
+
let rows = [];
|
|
7661
|
+
const printed = new Set;
|
|
7662
|
+
return {
|
|
7663
|
+
auth: (text) => write(`auth: ${text}`),
|
|
7664
|
+
setup: (state, seconds) => {
|
|
7665
|
+
if (state === "running")
|
|
7666
|
+
write("setup...");
|
|
7667
|
+
else if (state === "fail")
|
|
7668
|
+
write(`setup failed (${seconds}s)`);
|
|
7669
|
+
},
|
|
7670
|
+
runs: (r) => {
|
|
7671
|
+
rows = r;
|
|
7672
|
+
},
|
|
7673
|
+
update: () => {
|
|
7674
|
+
for (const r of rows) {
|
|
7675
|
+
if (printed.has(r) || r.state === "queued" || r.state === "running")
|
|
7676
|
+
continue;
|
|
7677
|
+
printed.add(r);
|
|
7678
|
+
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
7679
|
+
const u = `$${(r.cost ?? 0).toFixed(2)}, ${r.turns} turns, ${r.seconds ?? 0}s`;
|
|
7680
|
+
write(`${label} ${r.test}#${r.n} ${r.reason} (${u})`);
|
|
7681
|
+
}
|
|
7682
|
+
},
|
|
7683
|
+
done: ({ runsDir, rows: all }) => {
|
|
7684
|
+
const c = counts(all);
|
|
7685
|
+
const t = total(all);
|
|
7686
|
+
const tr = tier(all);
|
|
7687
|
+
write("");
|
|
7688
|
+
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs, $${t.cost.toFixed(2)})${tr ? `, tier ${tr}` : ""}`);
|
|
7689
|
+
write(`runs: ${runsDir} (end_goal is for you to judge: read <test>/<n>/transcript.txt)`);
|
|
7690
|
+
}
|
|
7691
|
+
};
|
|
7692
|
+
}
|
|
7693
|
+
function ttyUI(out, opts) {
|
|
7694
|
+
const p = painter(opts.color);
|
|
7695
|
+
let rows = [];
|
|
7696
|
+
let setup = null;
|
|
7697
|
+
let authText = "";
|
|
7698
|
+
let frame = 0;
|
|
7699
|
+
let drawn = 0;
|
|
7700
|
+
let finished = null;
|
|
7701
|
+
const width = () => Math.min(out.columns || 80, 100);
|
|
7702
|
+
const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
7703
|
+
const statusWord = (r) => {
|
|
7704
|
+
switch (r.state) {
|
|
7705
|
+
case "pass":
|
|
7706
|
+
return p(C.green, "PASS ");
|
|
7707
|
+
case "fail":
|
|
7708
|
+
return p(C.red, "FAIL ");
|
|
7709
|
+
case "error":
|
|
7710
|
+
return p(C.amber, "NOT GRADED");
|
|
7711
|
+
case "running":
|
|
7712
|
+
return p(C.ember, "running ");
|
|
7713
|
+
default:
|
|
7714
|
+
return p(C.gray, "queued ");
|
|
7715
|
+
}
|
|
7716
|
+
};
|
|
7717
|
+
const glyph = (r) => {
|
|
7718
|
+
switch (r.state) {
|
|
7719
|
+
case "pass":
|
|
7720
|
+
return p(C.green, "●");
|
|
7721
|
+
case "fail":
|
|
7722
|
+
return p(C.red, "✗");
|
|
7723
|
+
case "error":
|
|
7724
|
+
return p(C.amber, "?");
|
|
7725
|
+
case "running":
|
|
7726
|
+
return p(C.ember, SPIN[frame % SPIN.length]);
|
|
7727
|
+
default:
|
|
7728
|
+
return p(C.gray, "○");
|
|
7729
|
+
}
|
|
7730
|
+
};
|
|
7731
|
+
const lines = () => {
|
|
7732
|
+
const w = width();
|
|
7733
|
+
const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
|
|
7734
|
+
const c = counts(rows);
|
|
7735
|
+
const doneCount = c.pass + c.fail + c.error;
|
|
7736
|
+
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
7737
|
+
const title = ` \uD83E\uDD8B firegoat eval · ${opts.project}`;
|
|
7738
|
+
const out = [
|
|
7739
|
+
`${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)}`,
|
|
7740
|
+
rule
|
|
7741
|
+
];
|
|
7742
|
+
if (authText)
|
|
7743
|
+
out.push(p(C.gray, ` auth ${authText}`));
|
|
7744
|
+
if (setup) {
|
|
7745
|
+
const mark = setup.state === "ok" ? p(C.green, "✓") : setup.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
7746
|
+
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` : ""}`);
|
|
7747
|
+
}
|
|
7748
|
+
rows.forEach((r, i) => {
|
|
7749
|
+
out.push(p(C.gray, " │"));
|
|
7750
|
+
const num = String(i + 1).padStart(2, "0");
|
|
7751
|
+
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
7752
|
+
let detail = "";
|
|
7753
|
+
if (r.state === "running")
|
|
7754
|
+
detail = p(C.gray, `turn ${r.turns}`);
|
|
7755
|
+
else if (r.state === "pass" || r.state === "fail")
|
|
7756
|
+
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
|
|
7757
|
+
else if (r.state === "error")
|
|
7758
|
+
detail = p(C.gray, usage(r));
|
|
7759
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
7760
|
+
if (r.state === "running" && r.command)
|
|
7761
|
+
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
7762
|
+
if (r.state === "fail")
|
|
7763
|
+
for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
|
|
7764
|
+
out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
|
|
7765
|
+
out.push(p(C.gray, ` │ ${ck.error ?? `exit ${ck.code}`}`));
|
|
7766
|
+
}
|
|
7767
|
+
if (r.state === "error")
|
|
7768
|
+
out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
|
|
7769
|
+
});
|
|
7770
|
+
out.push(rule);
|
|
7771
|
+
const t = total(rows);
|
|
7772
|
+
if (!finished) {
|
|
7773
|
+
const track = Math.max(10, w - 34);
|
|
7774
|
+
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
7775
|
+
const goat = `${"·".repeat(at)}\uD83D\uDC10${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
7776
|
+
out.push(` ${p(C.ember, goat)} ${p(C.gray, `${doneCount}/${rows.length} · $${t.cost.toFixed(2)} so far`)}`);
|
|
7777
|
+
return out;
|
|
7778
|
+
}
|
|
7779
|
+
const tr = tier(rows);
|
|
7780
|
+
const goated = c.pass === rows.length && rows.length > 0;
|
|
7781
|
+
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
7782
|
+
const right = goated ? p(C.ember, `\uD83D\uDC10 GOATED · tier ${tr}`) : tr ? `tier ${tr}` : "";
|
|
7783
|
+
out.push(` ${goated ? p(C.green, `${rows.length}/${rows.length} runs passed their checks`) : tally}${" ".repeat(Math.max(2, w - tally.length - 22))}${right}`);
|
|
7784
|
+
out.push(p(C.gray, ` total $${t.cost.toFixed(2)} · ${t.turns} turns · ${t.time}`));
|
|
7785
|
+
out.push(p(C.gray, ` runs ${relative2(process.cwd(), finished.runsDir)}/`));
|
|
7786
|
+
out.push(` checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
7787
|
+
return out;
|
|
7788
|
+
};
|
|
7789
|
+
const rowsOf = (line) => {
|
|
7790
|
+
const visible = line.replace(ANSI, "").replace(/🐐|🦋/g, "gg");
|
|
7791
|
+
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
7792
|
+
};
|
|
7793
|
+
const render = () => {
|
|
7794
|
+
const text = lines();
|
|
7795
|
+
let s = drawn > 0 ? `\x1B[${drawn}A\x1B[J` : "";
|
|
7796
|
+
s += `${text.join(`
|
|
7797
|
+
`)}
|
|
7798
|
+
`;
|
|
7799
|
+
out.write(s);
|
|
7800
|
+
drawn = text.reduce((n, l) => n + rowsOf(l), 0);
|
|
7801
|
+
};
|
|
7802
|
+
const timer = setInterval(() => {
|
|
7803
|
+
frame++;
|
|
7804
|
+
render();
|
|
7805
|
+
}, 120);
|
|
7806
|
+
timer.unref();
|
|
7807
|
+
return {
|
|
7808
|
+
auth: (text) => {
|
|
7809
|
+
authText = text;
|
|
7810
|
+
},
|
|
7811
|
+
setup: (state, seconds) => {
|
|
7812
|
+
setup = { state, seconds };
|
|
7813
|
+
render();
|
|
7814
|
+
},
|
|
7815
|
+
runs: (r) => {
|
|
7816
|
+
rows = r;
|
|
7817
|
+
render();
|
|
7818
|
+
},
|
|
7819
|
+
update: render,
|
|
7820
|
+
done: ({ runId, runsDir }) => {
|
|
7821
|
+
clearInterval(timer);
|
|
7822
|
+
finished = { runId, runsDir };
|
|
7823
|
+
render();
|
|
7824
|
+
}
|
|
7825
|
+
};
|
|
7826
|
+
}
|
|
7827
|
+
|
|
7482
7828
|
// src/which.ts
|
|
7483
|
-
import { accessSync, constants, statSync } from "node:fs";
|
|
7484
|
-
import { delimiter, join as
|
|
7829
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
7830
|
+
import { delimiter, join as join3, resolve } from "node:path";
|
|
7485
7831
|
function resolveBin(bin) {
|
|
7486
7832
|
const executable = (p) => {
|
|
7487
7833
|
try {
|
|
7488
7834
|
accessSync(p, constants.X_OK);
|
|
7489
|
-
return
|
|
7835
|
+
return statSync2(p).isFile();
|
|
7490
7836
|
} catch {
|
|
7491
7837
|
return false;
|
|
7492
7838
|
}
|
|
@@ -7494,8 +7840,8 @@ function resolveBin(bin) {
|
|
|
7494
7840
|
if (bin.includes("/"))
|
|
7495
7841
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7496
7842
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
7497
|
-
if (dir && executable(
|
|
7498
|
-
return
|
|
7843
|
+
if (dir && executable(join3(dir, bin)))
|
|
7844
|
+
return join3(dir, bin);
|
|
7499
7845
|
}
|
|
7500
7846
|
return;
|
|
7501
7847
|
}
|
|
@@ -7550,25 +7896,16 @@ function parseArgs(args) {
|
|
|
7550
7896
|
return opts;
|
|
7551
7897
|
}
|
|
7552
7898
|
function findSession(configDir, sessionId) {
|
|
7553
|
-
const projects =
|
|
7899
|
+
const projects = join4(configDir, "projects");
|
|
7554
7900
|
if (!existsSync2(projects))
|
|
7555
7901
|
return null;
|
|
7556
|
-
for (const d of
|
|
7557
|
-
const f =
|
|
7902
|
+
for (const d of readdirSync2(projects)) {
|
|
7903
|
+
const f = join4(projects, d, `${sessionId}.jsonl`);
|
|
7558
7904
|
if (existsSync2(f))
|
|
7559
7905
|
return f;
|
|
7560
7906
|
}
|
|
7561
7907
|
return null;
|
|
7562
7908
|
}
|
|
7563
|
-
function usage(r) {
|
|
7564
|
-
const parts = [];
|
|
7565
|
-
if (r.cost !== null)
|
|
7566
|
-
parts.push(`$${r.cost.toFixed(2)}`);
|
|
7567
|
-
if (r.num_turns !== null)
|
|
7568
|
-
parts.push(`${r.num_turns} turns`);
|
|
7569
|
-
parts.push(`${Math.round(r.duration_ms / 1000)}s`);
|
|
7570
|
-
return parts.join(", ");
|
|
7571
|
-
}
|
|
7572
7909
|
function newRunId() {
|
|
7573
7910
|
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
7574
7911
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
@@ -7576,8 +7913,8 @@ function newRunId() {
|
|
|
7576
7913
|
async function runOne(opts) {
|
|
7577
7914
|
const { test, n } = opts;
|
|
7578
7915
|
const started = Date.now();
|
|
7579
|
-
const dir =
|
|
7580
|
-
const workdir =
|
|
7916
|
+
const dir = join4(opts.runDir, test.name, String(n));
|
|
7917
|
+
const workdir = join4(dir, "work");
|
|
7581
7918
|
mkdirSync2(workdir, { recursive: true });
|
|
7582
7919
|
const r = {
|
|
7583
7920
|
test: test.name,
|
|
@@ -7590,8 +7927,9 @@ async function runOne(opts) {
|
|
|
7590
7927
|
dir,
|
|
7591
7928
|
workdir,
|
|
7592
7929
|
session: null,
|
|
7593
|
-
|
|
7594
|
-
|
|
7930
|
+
transcript: join4(dir, "transcript.txt"),
|
|
7931
|
+
stderr: join4(dir, "stderr.txt"),
|
|
7932
|
+
checksLog: join4(dir, "checks.txt"),
|
|
7595
7933
|
num_turns: null,
|
|
7596
7934
|
cost: null,
|
|
7597
7935
|
duration_ms: 0
|
|
@@ -7600,7 +7938,7 @@ async function runOne(opts) {
|
|
|
7600
7938
|
r.status = status;
|
|
7601
7939
|
r.reason = reason;
|
|
7602
7940
|
r.duration_ms = Date.now() - started;
|
|
7603
|
-
|
|
7941
|
+
writeFileSync3(join4(dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
7604
7942
|
`);
|
|
7605
7943
|
return r;
|
|
7606
7944
|
};
|
|
@@ -7611,7 +7949,7 @@ async function runOne(opts) {
|
|
|
7611
7949
|
commands: opts.workdirSetup,
|
|
7612
7950
|
cwd: workdir,
|
|
7613
7951
|
env: stepEnv,
|
|
7614
|
-
logPath:
|
|
7952
|
+
logPath: join4(dir, "workdir_setup.txt")
|
|
7615
7953
|
});
|
|
7616
7954
|
if (error)
|
|
7617
7955
|
return done("error", `not graded: ${error}`);
|
|
@@ -7623,11 +7961,19 @@ async function runOne(opts) {
|
|
|
7623
7961
|
cwd: workdir,
|
|
7624
7962
|
env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
|
|
7625
7963
|
stderr,
|
|
7626
|
-
timeoutMs: test.timeout * 1000
|
|
7964
|
+
timeoutMs: test.timeout * 1000,
|
|
7965
|
+
onEvent: opts.onEvent
|
|
7627
7966
|
});
|
|
7628
7967
|
await new Promise((res) => stderr.end(res));
|
|
7629
7968
|
if (outcome.sessionId)
|
|
7630
7969
|
r.session = findSession(opts.configDir, outcome.sessionId);
|
|
7970
|
+
writeTranscript({
|
|
7971
|
+
path: r.transcript,
|
|
7972
|
+
title: `${test.name}#${n}`,
|
|
7973
|
+
prompt: r.prompt,
|
|
7974
|
+
events: outcome.events,
|
|
7975
|
+
workdir
|
|
7976
|
+
});
|
|
7631
7977
|
r.num_turns = outcome.result?.num_turns ?? null;
|
|
7632
7978
|
r.cost = outcome.result?.total_cost_usd ?? null;
|
|
7633
7979
|
if (!outcome.ok)
|
|
@@ -7640,7 +7986,11 @@ async function runOne(opts) {
|
|
|
7640
7986
|
r.checks.push(await runShell({
|
|
7641
7987
|
run: render(c.run, opts.vars),
|
|
7642
7988
|
cwd: workdir,
|
|
7643
|
-
env: {
|
|
7989
|
+
env: {
|
|
7990
|
+
...stepEnv,
|
|
7991
|
+
FIREGOAT_SESSION: r.session,
|
|
7992
|
+
FIREGOAT_TRANSCRIPT: r.transcript
|
|
7993
|
+
},
|
|
7644
7994
|
timeoutMs: CHECK_TIMEOUT_MS,
|
|
7645
7995
|
log
|
|
7646
7996
|
}));
|
|
@@ -7673,6 +8023,8 @@ async function evalCommand(args, root) {
|
|
|
7673
8023
|
throw e;
|
|
7674
8024
|
}
|
|
7675
8025
|
const say = opts.json ? console.error : console.log;
|
|
8026
|
+
const stream = opts.json ? process.stderr : process.stdout;
|
|
8027
|
+
const ui = stream.isTTY ? ttyUI(stream, { project: basename(root), color: !process.env.NO_COLOR }) : plainUI(say);
|
|
7676
8028
|
let config;
|
|
7677
8029
|
try {
|
|
7678
8030
|
config = loadConfig(root);
|
|
@@ -7700,9 +8052,9 @@ async function evalCommand(args, root) {
|
|
|
7700
8052
|
if (!claudePath)
|
|
7701
8053
|
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.`);
|
|
7702
8054
|
const firegoatDir = ensureFiregoatDir(root);
|
|
7703
|
-
const configDir =
|
|
8055
|
+
const configDir = join4(firegoatDir, ".claude");
|
|
7704
8056
|
mkdirSync2(configDir, { recursive: true });
|
|
7705
|
-
|
|
8057
|
+
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");
|
|
7706
8058
|
const loginProblem = checkLogin({
|
|
7707
8059
|
bin: claudePath,
|
|
7708
8060
|
env: claudeEnv(configDir),
|
|
@@ -7711,8 +8063,8 @@ async function evalCommand(args, root) {
|
|
|
7711
8063
|
if (loginProblem)
|
|
7712
8064
|
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
7713
8065
|
const runId = newRunId();
|
|
7714
|
-
const runDir =
|
|
7715
|
-
const out =
|
|
8066
|
+
const runDir = join4(firegoatDir, "runs", runId);
|
|
8067
|
+
const out = join4(runDir, "out");
|
|
7716
8068
|
mkdirSync2(out, { recursive: true });
|
|
7717
8069
|
const stepEnv = {
|
|
7718
8070
|
...process.env,
|
|
@@ -7720,22 +8072,49 @@ async function evalCommand(args, root) {
|
|
|
7720
8072
|
FIREGOAT_OUT: out,
|
|
7721
8073
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
7722
8074
|
};
|
|
8075
|
+
const rows = tests.flatMap((t) => Array.from({ length: runs }, (_, i) => ({
|
|
8076
|
+
test: t.name,
|
|
8077
|
+
n: i + 1,
|
|
8078
|
+
state: "queued",
|
|
8079
|
+
turns: 0
|
|
8080
|
+
})));
|
|
7723
8081
|
if (config.setup.length > 0) {
|
|
7724
|
-
|
|
8082
|
+
const setupStarted = Date.now();
|
|
8083
|
+
ui.setup("running");
|
|
7725
8084
|
const error = await runSteps({
|
|
7726
8085
|
what: "setup",
|
|
7727
8086
|
commands: config.setup,
|
|
7728
8087
|
cwd: root,
|
|
7729
8088
|
env: stepEnv,
|
|
7730
|
-
logPath:
|
|
8089
|
+
logPath: join4(runDir, "setup.txt")
|
|
7731
8090
|
});
|
|
8091
|
+
const secs = Math.round((Date.now() - setupStarted) / 1000);
|
|
8092
|
+
ui.setup(error ? "fail" : "ok", secs);
|
|
7732
8093
|
if (error)
|
|
7733
8094
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
7734
8095
|
}
|
|
8096
|
+
ui.runs(rows);
|
|
7735
8097
|
const results = [];
|
|
7736
8098
|
for (const test of tests)
|
|
7737
8099
|
for (let n = 1;n <= runs; n++) {
|
|
8100
|
+
const row = rows.find((x) => x.test === test.name && x.n === n);
|
|
8101
|
+
row.state = "running";
|
|
8102
|
+
ui.update();
|
|
7738
8103
|
const r = await runOne({
|
|
8104
|
+
onEvent: (e) => {
|
|
8105
|
+
if (e.type !== "assistant")
|
|
8106
|
+
return;
|
|
8107
|
+
row.turns++;
|
|
8108
|
+
const content = e.message?.content;
|
|
8109
|
+
if (!Array.isArray(content))
|
|
8110
|
+
return;
|
|
8111
|
+
for (const b of content) {
|
|
8112
|
+
const cmd = b.input?.command;
|
|
8113
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
8114
|
+
row.command = cmd.split(`
|
|
8115
|
+
`)[0];
|
|
8116
|
+
}
|
|
8117
|
+
},
|
|
7739
8118
|
test,
|
|
7740
8119
|
n,
|
|
7741
8120
|
runDir,
|
|
@@ -7746,18 +8125,24 @@ async function evalCommand(args, root) {
|
|
|
7746
8125
|
vars
|
|
7747
8126
|
});
|
|
7748
8127
|
results.push(r);
|
|
7749
|
-
|
|
8128
|
+
Object.assign(row, {
|
|
8129
|
+
state: r.status,
|
|
8130
|
+
turns: r.num_turns ?? row.turns,
|
|
8131
|
+
reason: r.reason,
|
|
8132
|
+
checks: r.checks,
|
|
8133
|
+
cost: r.cost,
|
|
8134
|
+
seconds: Math.round(r.duration_ms / 1000),
|
|
8135
|
+
command: undefined
|
|
8136
|
+
});
|
|
8137
|
+
ui.update();
|
|
7750
8138
|
}
|
|
7751
8139
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
7752
8140
|
for (const r of results)
|
|
7753
8141
|
totals[r.status]++;
|
|
7754
8142
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
7755
|
-
|
|
8143
|
+
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
7756
8144
|
`);
|
|
7757
|
-
|
|
7758
|
-
say(`
|
|
7759
|
-
${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${results.length} runs, $${cost.toFixed(2)})`);
|
|
7760
|
-
say(`runs: ${runDir} (end_goal is for you to judge: see run.json per run)`);
|
|
8145
|
+
ui.done({ runId, runsDir: runDir, rows });
|
|
7761
8146
|
if (opts.json)
|
|
7762
8147
|
console.log(JSON.stringify(doc, null, 2));
|
|
7763
8148
|
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
@@ -7765,8 +8150,8 @@ ${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${resu
|
|
|
7765
8150
|
|
|
7766
8151
|
// src/init.ts
|
|
7767
8152
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7768
|
-
import { existsSync as existsSync3, writeFileSync as
|
|
7769
|
-
import { basename, join as
|
|
8153
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8154
|
+
import { basename as basename2, join as join5 } from "node:path";
|
|
7770
8155
|
import { createInterface } from "node:readline/promises";
|
|
7771
8156
|
var SKILLS_ARGV = [
|
|
7772
8157
|
"skills",
|
|
@@ -7869,48 +8254,73 @@ ${INIT_HELP}`);
|
|
|
7869
8254
|
return EXIT.usage;
|
|
7870
8255
|
}
|
|
7871
8256
|
}
|
|
8257
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8258
|
+
const color = interactive && !process.env.NO_COLOR;
|
|
8259
|
+
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
8260
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
8261
|
+
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
8262
|
+
const step = (n, label, status) => console.log(` ${paint("32", "●")} ${paint("90", String(n).padStart(2, "0"))} ${label.padEnd(28)}${paint("90", status)}`);
|
|
8263
|
+
if (interactive) {
|
|
8264
|
+
const title = " \uD83E\uDD8B firegoat init";
|
|
8265
|
+
const tag = "Get agent-ready.";
|
|
8266
|
+
console.log(`${paint("38;5;208", " \uD83E\uDD8B")} firegoat init${" ".repeat(Math.max(2, width - title.length - tag.length - 2))}${paint("90", tag)}`);
|
|
8267
|
+
console.log(rule);
|
|
8268
|
+
}
|
|
7872
8269
|
ensureFiregoatDir(cwd);
|
|
7873
|
-
const configPath =
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
8270
|
+
const configPath = join5(cwd, CONFIG_FILE);
|
|
8271
|
+
const keep = existsSync3(configPath) && !force;
|
|
8272
|
+
if (!keep)
|
|
8273
|
+
writeFileSync4(configPath, template(basename2(cwd)));
|
|
8274
|
+
if (interactive) {
|
|
8275
|
+
step(1, CONFIG_FILE, keep ? "kept (--force replaces it)" : "created");
|
|
8276
|
+
console.log(paint("90", " │"));
|
|
8277
|
+
step(2, ".firegoat/", "ready");
|
|
8278
|
+
} else
|
|
8279
|
+
console.log(keep ? `kept ${CONFIG_FILE} (--force replaces it)` : `wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
8280
|
+
if (skills && interactive) {
|
|
8281
|
+
console.log(paint("90", " │"));
|
|
8282
|
+
skills = await askYes(` ${paint("38;5;208", "◉")} ${paint("90", "03")} Install the firegoat-onboarding skill?`);
|
|
8283
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
8284
|
+
}
|
|
7883
8285
|
if (skills) {
|
|
7884
|
-
|
|
8286
|
+
if (!interactive)
|
|
8287
|
+
console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
|
|
7885
8288
|
const env = { ...process.env };
|
|
7886
8289
|
delete env.npm_config_package;
|
|
7887
8290
|
const r = spawnSync2("npx", ["-y", ...SKILLS_ARGV], {
|
|
7888
8291
|
cwd,
|
|
7889
8292
|
env,
|
|
7890
|
-
stdio:
|
|
8293
|
+
stdio: "pipe",
|
|
7891
8294
|
encoding: "utf8"
|
|
7892
8295
|
});
|
|
7893
8296
|
if (r.error || r.status !== 0) {
|
|
7894
|
-
|
|
7895
|
-
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
8297
|
+
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
7896
8298
|
console.error(`firegoat init: installing the skills failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
|
|
7897
8299
|
return EXIT.notGraded;
|
|
7898
8300
|
}
|
|
7899
|
-
|
|
8301
|
+
if (interactive)
|
|
8302
|
+
step(3, "firegoat-onboarding skill", "installed → .claude/skills/");
|
|
8303
|
+
else
|
|
8304
|
+
console.log("installed skills firegoat-onboarding, suggesting-eval-prompts in .claude/skills/");
|
|
7900
8305
|
const ignored = spawnSync2("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
7901
8306
|
cwd
|
|
7902
8307
|
});
|
|
7903
8308
|
if (ignored.status === 0)
|
|
7904
8309
|
console.error("warning: git ignores .claude/skills, so teammates and CI won't get the skills. Commit them with: git add -f .claude/skills");
|
|
7905
|
-
}
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
${AGENT_PROMPT}
|
|
7910
|
-
` : `
|
|
8310
|
+
} else if (interactive)
|
|
8311
|
+
step(3, "firegoat-onboarding skill", "skipped");
|
|
8312
|
+
if (!interactive) {
|
|
8313
|
+
console.log(`
|
|
7911
8314
|
Prompt for your coding agent: ${AGENT_PROMPT}`);
|
|
7912
|
-
|
|
7913
|
-
|
|
8315
|
+
return EXIT.pass;
|
|
8316
|
+
}
|
|
8317
|
+
console.log(rule);
|
|
8318
|
+
console.log(` next: hand this to your coding agent
|
|
8319
|
+
`);
|
|
8320
|
+
console.log(AGENT_PROMPT);
|
|
8321
|
+
console.log("");
|
|
8322
|
+
const copied = copyToClipboard(AGENT_PROMPT);
|
|
8323
|
+
console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}then: ${paint("36", "npx firegoat eval")}`);
|
|
7914
8324
|
return EXIT.pass;
|
|
7915
8325
|
}
|
|
7916
8326
|
|