firegoat 0.10.0 → 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 +1 -1
- package/dist/cli.js +339 -43
- 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
|
|
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"
|
|
@@ -7172,7 +7172,7 @@ import {
|
|
|
7172
7172
|
readdirSync as readdirSync2,
|
|
7173
7173
|
writeFileSync as writeFileSync3
|
|
7174
7174
|
} from "node:fs";
|
|
7175
|
-
import { delimiter as delimiter2, join as join4 } from "node:path";
|
|
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,7 +7234,19 @@ 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
|
});
|
|
@@ -7581,6 +7594,237 @@ ${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
|
7581
7594
|
`);
|
|
7582
7595
|
}
|
|
7583
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
|
+
|
|
7584
7828
|
// src/which.ts
|
|
7585
7829
|
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
7586
7830
|
import { delimiter, join as join3, resolve } from "node:path";
|
|
@@ -7662,15 +7906,6 @@ function findSession(configDir, sessionId) {
|
|
|
7662
7906
|
}
|
|
7663
7907
|
return null;
|
|
7664
7908
|
}
|
|
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
7909
|
function newRunId() {
|
|
7675
7910
|
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
7676
7911
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
@@ -7726,7 +7961,8 @@ async function runOne(opts) {
|
|
|
7726
7961
|
cwd: workdir,
|
|
7727
7962
|
env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
|
|
7728
7963
|
stderr,
|
|
7729
|
-
timeoutMs: test.timeout * 1000
|
|
7964
|
+
timeoutMs: test.timeout * 1000,
|
|
7965
|
+
onEvent: opts.onEvent
|
|
7730
7966
|
});
|
|
7731
7967
|
await new Promise((res) => stderr.end(res));
|
|
7732
7968
|
if (outcome.sessionId)
|
|
@@ -7787,6 +8023,8 @@ async function evalCommand(args, root) {
|
|
|
7787
8023
|
throw e;
|
|
7788
8024
|
}
|
|
7789
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);
|
|
7790
8028
|
let config;
|
|
7791
8029
|
try {
|
|
7792
8030
|
config = loadConfig(root);
|
|
@@ -7816,7 +8054,7 @@ async function evalCommand(args, root) {
|
|
|
7816
8054
|
const firegoatDir = ensureFiregoatDir(root);
|
|
7817
8055
|
const configDir = join4(firegoatDir, ".claude");
|
|
7818
8056
|
mkdirSync2(configDir, { recursive: true });
|
|
7819
|
-
|
|
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");
|
|
7820
8058
|
const loginProblem = checkLogin({
|
|
7821
8059
|
bin: claudePath,
|
|
7822
8060
|
env: claudeEnv(configDir),
|
|
@@ -7834,8 +8072,15 @@ async function evalCommand(args, root) {
|
|
|
7834
8072
|
FIREGOAT_OUT: out,
|
|
7835
8073
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
7836
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
|
+
})));
|
|
7837
8081
|
if (config.setup.length > 0) {
|
|
7838
|
-
|
|
8082
|
+
const setupStarted = Date.now();
|
|
8083
|
+
ui.setup("running");
|
|
7839
8084
|
const error = await runSteps({
|
|
7840
8085
|
what: "setup",
|
|
7841
8086
|
commands: config.setup,
|
|
@@ -7843,13 +8088,33 @@ async function evalCommand(args, root) {
|
|
|
7843
8088
|
env: stepEnv,
|
|
7844
8089
|
logPath: join4(runDir, "setup.txt")
|
|
7845
8090
|
});
|
|
8091
|
+
const secs = Math.round((Date.now() - setupStarted) / 1000);
|
|
8092
|
+
ui.setup(error ? "fail" : "ok", secs);
|
|
7846
8093
|
if (error)
|
|
7847
8094
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
7848
8095
|
}
|
|
8096
|
+
ui.runs(rows);
|
|
7849
8097
|
const results = [];
|
|
7850
8098
|
for (const test of tests)
|
|
7851
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();
|
|
7852
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
|
+
},
|
|
7853
8118
|
test,
|
|
7854
8119
|
n,
|
|
7855
8120
|
runDir,
|
|
@@ -7860,7 +8125,16 @@ async function evalCommand(args, root) {
|
|
|
7860
8125
|
vars
|
|
7861
8126
|
});
|
|
7862
8127
|
results.push(r);
|
|
7863
|
-
|
|
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();
|
|
7864
8138
|
}
|
|
7865
8139
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
7866
8140
|
for (const r of results)
|
|
@@ -7868,10 +8142,7 @@ async function evalCommand(args, root) {
|
|
|
7868
8142
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
7869
8143
|
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
7870
8144
|
`);
|
|
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)`);
|
|
8145
|
+
ui.done({ runId, runsDir: runDir, rows });
|
|
7875
8146
|
if (opts.json)
|
|
7876
8147
|
console.log(JSON.stringify(doc, null, 2));
|
|
7877
8148
|
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
@@ -7880,7 +8151,7 @@ ${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${resu
|
|
|
7880
8151
|
// src/init.ts
|
|
7881
8152
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7882
8153
|
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7883
|
-
import { basename, join as join5 } from "node:path";
|
|
8154
|
+
import { basename as basename2, join as join5 } from "node:path";
|
|
7884
8155
|
import { createInterface } from "node:readline/promises";
|
|
7885
8156
|
var SKILLS_ARGV = [
|
|
7886
8157
|
"skills",
|
|
@@ -7983,48 +8254,73 @@ ${INIT_HELP}`);
|
|
|
7983
8254
|
return EXIT.usage;
|
|
7984
8255
|
}
|
|
7985
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
|
+
}
|
|
7986
8269
|
ensureFiregoatDir(cwd);
|
|
7987
8270
|
const configPath = join5(cwd, CONFIG_FILE);
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
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
|
+
}
|
|
7997
8285
|
if (skills) {
|
|
7998
|
-
|
|
8286
|
+
if (!interactive)
|
|
8287
|
+
console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
|
|
7999
8288
|
const env = { ...process.env };
|
|
8000
8289
|
delete env.npm_config_package;
|
|
8001
8290
|
const r = spawnSync2("npx", ["-y", ...SKILLS_ARGV], {
|
|
8002
8291
|
cwd,
|
|
8003
8292
|
env,
|
|
8004
|
-
stdio:
|
|
8293
|
+
stdio: "pipe",
|
|
8005
8294
|
encoding: "utf8"
|
|
8006
8295
|
});
|
|
8007
8296
|
if (r.error || r.status !== 0) {
|
|
8008
|
-
|
|
8009
|
-
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
8297
|
+
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
8010
8298
|
console.error(`firegoat init: installing the skills failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8011
8299
|
return EXIT.notGraded;
|
|
8012
8300
|
}
|
|
8013
|
-
|
|
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/");
|
|
8014
8305
|
const ignored = spawnSync2("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
8015
8306
|
cwd
|
|
8016
8307
|
});
|
|
8017
8308
|
if (ignored.status === 0)
|
|
8018
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");
|
|
8019
|
-
}
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
${AGENT_PROMPT}
|
|
8024
|
-
` : `
|
|
8310
|
+
} else if (interactive)
|
|
8311
|
+
step(3, "firegoat-onboarding skill", "skipped");
|
|
8312
|
+
if (!interactive) {
|
|
8313
|
+
console.log(`
|
|
8025
8314
|
Prompt for your coding agent: ${AGENT_PROMPT}`);
|
|
8026
|
-
|
|
8027
|
-
|
|
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")}`);
|
|
8028
8324
|
return EXIT.pass;
|
|
8029
8325
|
}
|
|
8030
8326
|
|