firegoat 0.29.0 → 0.30.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 +2 -0
- package/dist/cli.js +145 -92
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ In a terminal it asks where to install the firegoat-onboarding skill (global by
|
|
|
22
22
|
npx firegoat@latest eval
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Run by an agent, eval ends with a results block to paste as-is (also in `.firegoat/runs/<runId>/results.txt`): ask it to show you the results table.
|
|
26
|
+
|
|
25
27
|
## Example
|
|
26
28
|
|
|
27
29
|
```yaml
|
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.30.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -9203,6 +9203,14 @@ says "N uploads failed" on stderr. A file over 20 MiB fails that run's upload.
|
|
|
9203
9203
|
Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
9204
9204
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
9205
9205
|
|
|
9206
|
+
Results block: without a terminal (an agent, a pipe, CI) and without --json, eval
|
|
9207
|
+
ends with the terminal's final timeline as one plain block (no color, at most 100
|
|
9208
|
+
columns): the logo header, a row per run (status, a ramp of one block per agent turn
|
|
9209
|
+
ending ▲ pass, ╳ fail or ╌ not graded, checks passed, cost, turns, time), its failed
|
|
9210
|
+
checks and trace URL, the totals and the tier ("GOATED · tier S" when all pass). It
|
|
9211
|
+
sits between two lines of ═, after a "Results (paste the block ...)" line; the last
|
|
9212
|
+
line says where it is saved, results.txt. Show it to your user as-is in a code block.
|
|
9213
|
+
|
|
9206
9214
|
Output: .firegoat/runs/<runId>/
|
|
9207
9215
|
out A symlink to $FIREGOAT_OUT,
|
|
9208
9216
|
~/.firegoat/runs/<hash>/<random>/out
|
|
@@ -9211,6 +9219,7 @@ Output: .firegoat/runs/<runId>/
|
|
|
9211
9219
|
repo), harnesses, upload {origin, email} (null when
|
|
9212
9220
|
not uploading), runs (every run.json), then the
|
|
9213
9221
|
verdict: totals, tier, status and exitCode
|
|
9222
|
+
results.txt The results block, also with --json or in a terminal
|
|
9214
9223
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
9215
9224
|
commit, harnesses, passed, failed, notGraded, cost (null when
|
|
9216
9225
|
any run's cost is unknown), uploaded, uploadFailed
|
|
@@ -10313,17 +10322,9 @@ function plainUI(write) {
|
|
|
10313
10322
|
}
|
|
10314
10323
|
};
|
|
10315
10324
|
}
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
let build = null;
|
|
10320
|
-
let authText = "";
|
|
10321
|
-
const notes = [];
|
|
10322
|
-
let frame = 0;
|
|
10323
|
-
let drawn = 0;
|
|
10324
|
-
let finished = null;
|
|
10325
|
-
const width = () => Math.min(out.columns || 80, 100);
|
|
10326
|
-
const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
10325
|
+
var cut = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
10326
|
+
function timeline(t, p, w) {
|
|
10327
|
+
const { rows, build, finished, frame } = t;
|
|
10327
10328
|
const statusWord = (r) => {
|
|
10328
10329
|
switch (r.state) {
|
|
10329
10330
|
case "pass":
|
|
@@ -10352,77 +10353,109 @@ function ttyUI(out, opts) {
|
|
|
10352
10353
|
return p(C.gray, "○");
|
|
10353
10354
|
}
|
|
10354
10355
|
};
|
|
10355
|
-
const
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
10412
|
-
return out;
|
|
10413
|
-
}
|
|
10414
|
-
const tr = tier(rows);
|
|
10415
|
-
const goated = c.pass === rows.length && rows.length > 0;
|
|
10416
|
-
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10417
|
-
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
10418
|
-
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10419
|
-
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10420
|
-
const turns = totalTurns(rows);
|
|
10421
|
-
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
10422
|
-
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10423
|
-
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10356
|
+
const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
|
|
10357
|
+
const c = counts(rows);
|
|
10358
|
+
const doneCount = c.pass + c.fail + c.error;
|
|
10359
|
+
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
10360
|
+
const out = [
|
|
10361
|
+
...logoHeader(p, `firegoat ${t.version} eval · ${t.project}`, p(finished ? C.green : C.ember, badge), w),
|
|
10362
|
+
rule
|
|
10363
|
+
];
|
|
10364
|
+
if (t.authText)
|
|
10365
|
+
out.push(p(C.gray, ` Auth ${t.authText}`));
|
|
10366
|
+
for (const n of t.notes)
|
|
10367
|
+
out.push(p(C.gray, ` ${cut(n, w - 2)}`));
|
|
10368
|
+
if (build) {
|
|
10369
|
+
const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
10370
|
+
out.push(` ${build.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} Build${" ".repeat(Math.max(1, w - 20))}${mark}${build.seconds !== undefined ? ` ${build.seconds}s` : ""}`);
|
|
10371
|
+
}
|
|
10372
|
+
const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
|
|
10373
|
+
const harnessCols = column((r) => r.harness?.length ?? 0);
|
|
10374
|
+
const modelCols = column((r) => r.model?.length ?? 0);
|
|
10375
|
+
const detailCols = w - 1 - (47 + (harnessCols > 0 ? harnessCols + 1 : 0) + (modelCols > 0 ? modelCols + 1 : 0));
|
|
10376
|
+
rows.forEach((r, i) => {
|
|
10377
|
+
out.push(p(C.gray, " │"));
|
|
10378
|
+
const num = String(i + 1).padStart(2, "0");
|
|
10379
|
+
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
10380
|
+
const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
|
|
10381
|
+
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
10382
|
+
let detail = "";
|
|
10383
|
+
if (r.state === "running")
|
|
10384
|
+
detail = p(C.gray, `Turn ${r.turns}`);
|
|
10385
|
+
else if (r.state === "pass" || r.state === "fail")
|
|
10386
|
+
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, cut(usage(r), Math.max(1, detailCols - 12)))}`;
|
|
10387
|
+
else if (r.state === "error")
|
|
10388
|
+
detail = p(C.gray, cut(usage(r), Math.max(1, detailCols)));
|
|
10389
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
10390
|
+
if (r.state === "running" && r.command)
|
|
10391
|
+
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
10392
|
+
if (r.state === "fail")
|
|
10393
|
+
for (const ck of (r.checks ?? []).filter((x) => x.code !== 0)) {
|
|
10394
|
+
out.push(` ${p(C.gray, "│")} ${p(C.red, "✗")} ${cut(ck.run, w - 14)}`);
|
|
10395
|
+
out.push(p(C.gray, ` │ ${cut(ck.error ?? `exit ${ck.code}`, w - 14)}`));
|
|
10396
|
+
}
|
|
10397
|
+
if (r.state === "error")
|
|
10398
|
+
out.push(p(C.amber, ` ${cut(notGradedWhy(r.reason ?? ""), w - 12)}`));
|
|
10399
|
+
if (r.upload === "uploading")
|
|
10400
|
+
out.push(p(C.gray, " │ ↑ Uploading…"));
|
|
10401
|
+
else if (r.upload?.status === "uploaded")
|
|
10402
|
+
out.push(` ${p(C.gray, "│")} ${p(C.cyan, `↑ ${r.upload.url}`)}`);
|
|
10403
|
+
else if (r.upload)
|
|
10404
|
+
out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
|
|
10405
|
+
});
|
|
10406
|
+
out.push(rule);
|
|
10407
|
+
if (!finished) {
|
|
10408
|
+
const track = Math.max(10, w - 34);
|
|
10409
|
+
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
10410
|
+
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
10411
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
10424
10412
|
return out;
|
|
10425
|
-
}
|
|
10413
|
+
}
|
|
10414
|
+
const tr = tier(rows);
|
|
10415
|
+
const goated = c.pass === rows.length && rows.length > 0;
|
|
10416
|
+
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10417
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
10418
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10419
|
+
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10420
|
+
const turns = totalTurns(rows);
|
|
10421
|
+
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
10422
|
+
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10423
|
+
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10424
|
+
return out;
|
|
10425
|
+
}
|
|
10426
|
+
var RESULTS_WIDTH = 100;
|
|
10427
|
+
function resultsBlock(t) {
|
|
10428
|
+
return timeline({
|
|
10429
|
+
project: t.project,
|
|
10430
|
+
version: t.version,
|
|
10431
|
+
rows: t.rows,
|
|
10432
|
+
build: null,
|
|
10433
|
+
authText: "",
|
|
10434
|
+
notes: [],
|
|
10435
|
+
finished: { runId: t.runId, runsDir: t.runsDir, seconds: t.seconds },
|
|
10436
|
+
frame: 0
|
|
10437
|
+
}, painter(false), RESULTS_WIDTH);
|
|
10438
|
+
}
|
|
10439
|
+
function ttyUI(out, opts) {
|
|
10440
|
+
const p = painter(opts.color);
|
|
10441
|
+
let rows = [];
|
|
10442
|
+
let build = null;
|
|
10443
|
+
let authText = "";
|
|
10444
|
+
const notes = [];
|
|
10445
|
+
let frame = 0;
|
|
10446
|
+
let drawn = 0;
|
|
10447
|
+
let finished = null;
|
|
10448
|
+
const width = () => Math.min(out.columns || 80, 100);
|
|
10449
|
+
const lines = () => timeline({
|
|
10450
|
+
project: opts.project,
|
|
10451
|
+
version: opts.version,
|
|
10452
|
+
rows,
|
|
10453
|
+
build,
|
|
10454
|
+
authText,
|
|
10455
|
+
notes,
|
|
10456
|
+
finished,
|
|
10457
|
+
frame
|
|
10458
|
+
}, p, width());
|
|
10426
10459
|
const rowsOf = (line) => {
|
|
10427
10460
|
const visible = line.replace(ANSI, "");
|
|
10428
10461
|
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
@@ -11043,6 +11076,16 @@ function uploadedSummary(origin, results, runDir) {
|
|
|
11043
11076
|
for (const u of urls)
|
|
11044
11077
|
console.log(` ${u}`);
|
|
11045
11078
|
}
|
|
11079
|
+
var RESULTS_EDGE = "═".repeat(RESULTS_WIDTH);
|
|
11080
|
+
function printResults(block, file) {
|
|
11081
|
+
console.log("");
|
|
11082
|
+
console.log("Results (paste the block between the ═ lines as-is, in a code block):");
|
|
11083
|
+
console.log(RESULTS_EDGE);
|
|
11084
|
+
for (const line of block)
|
|
11085
|
+
console.log(line);
|
|
11086
|
+
console.log(RESULTS_EDGE);
|
|
11087
|
+
console.log(`Results block saved to ${file}`);
|
|
11088
|
+
}
|
|
11046
11089
|
async function evalCommand(args, root) {
|
|
11047
11090
|
if (args.includes("--help") || args.includes("-h")) {
|
|
11048
11091
|
console.log(EVAL_HELP);
|
|
@@ -11333,12 +11376,8 @@ async function evalCommand(args, root) {
|
|
|
11333
11376
|
uploadFailed: uploadsFailed
|
|
11334
11377
|
})}
|
|
11335
11378
|
`);
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
runsDir: runDir,
|
|
11339
|
-
rows,
|
|
11340
|
-
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
11341
|
-
});
|
|
11379
|
+
const seconds = Math.round((Date.now() - evalStarted) / 1000);
|
|
11380
|
+
ui.done({ runId, runsDir: runDir, rows, seconds });
|
|
11342
11381
|
if (upload && !opts.json)
|
|
11343
11382
|
uploadedSummary(upload.origin, results, runDir);
|
|
11344
11383
|
for (const r of results)
|
|
@@ -11350,6 +11389,20 @@ async function evalCommand(args, root) {
|
|
|
11350
11389
|
console.error(`firegoat eval: Warning: ${r.test}#${r.n} left ${r.leftover.length} process${r.leftover.length === 1 ? "" : "es"} running, stopped after cleanup: ${r.leftover.map((l) => `${l.pid} ${l.command}`).join("; ")} (run.json leftover)`);
|
|
11351
11390
|
if (uploadsFailed > 0)
|
|
11352
11391
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
11392
|
+
const block = resultsBlock({
|
|
11393
|
+
project: basename2(root),
|
|
11394
|
+
version: package_default.version,
|
|
11395
|
+
rows,
|
|
11396
|
+
runId,
|
|
11397
|
+
runsDir: runDir,
|
|
11398
|
+
seconds
|
|
11399
|
+
});
|
|
11400
|
+
const resultsFile = join10(runDir, "results.txt");
|
|
11401
|
+
writeFileSync5(resultsFile, `${block.join(`
|
|
11402
|
+
`)}
|
|
11403
|
+
`);
|
|
11404
|
+
if (!opts.json && !stream.isTTY)
|
|
11405
|
+
printResults(block, resultsFile);
|
|
11353
11406
|
if (opts.json)
|
|
11354
11407
|
console.log(JSON.stringify(doc, null, 2));
|
|
11355
11408
|
if (status === "error")
|
|
@@ -12049,7 +12102,7 @@ function copyToClipboard(text) {
|
|
|
12049
12102
|
});
|
|
12050
12103
|
}
|
|
12051
12104
|
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
12052
|
-
function
|
|
12105
|
+
function cut2(s, n) {
|
|
12053
12106
|
return s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}~`;
|
|
12054
12107
|
}
|
|
12055
12108
|
|
|
@@ -12064,8 +12117,8 @@ class Timeline {
|
|
|
12064
12117
|
line(mark, n, label, status = "") {
|
|
12065
12118
|
const head = ` ${mark} ${this.paint("90", String(n).padStart(2, "0"))} `;
|
|
12066
12119
|
if (!status)
|
|
12067
|
-
return head +
|
|
12068
|
-
return `${head}${label.padEnd(28)}${this.paint("90",
|
|
12120
|
+
return head + cut2(label, this.width - 9);
|
|
12121
|
+
return `${head}${label.padEnd(28)}${this.paint("90", cut2(status, this.width - 37))}`;
|
|
12069
12122
|
}
|
|
12070
12123
|
done(n, label, status) {
|
|
12071
12124
|
return this.line(this.paint("32", "●"), n, label, status);
|
|
@@ -12088,7 +12141,7 @@ class Timeline {
|
|
|
12088
12141
|
return select(block, options.length, (current) => [
|
|
12089
12142
|
this.line(this.paint("38;5;208", "◉"), n, question),
|
|
12090
12143
|
...options.map(([label, note], i) => {
|
|
12091
|
-
const text =
|
|
12144
|
+
const text = cut2(`${label.padEnd(cols)}${note}`, room);
|
|
12092
12145
|
return i === current ? `${Timeline.INDENT}${this.paint("38;5;208", `● ${text}`)}` : this.paint("90", `${Timeline.INDENT}○ ${text}`);
|
|
12093
12146
|
}),
|
|
12094
12147
|
this.paint("90", `${Timeline.INDENT}Up/down to move, enter to pick`)
|