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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +145 -92
  3. 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.29.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
- function ttyUI(out, opts) {
10317
- const p = painter(opts.color);
10318
- let rows = [];
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 lines = () => {
10356
- const w = width();
10357
- const rule = p(C.gray, ` ${"─".repeat(w - 2)}`);
10358
- const c = counts(rows);
10359
- const doneCount = c.pass + c.fail + c.error;
10360
- const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
10361
- const out = [
10362
- ...logoHeader(p, `firegoat ${opts.version} eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
10363
- rule
10364
- ];
10365
- if (authText)
10366
- out.push(p(C.gray, ` Auth ${authText}`));
10367
- for (const n of notes)
10368
- out.push(p(C.gray, ` ${cut(n, w - 2)}`));
10369
- if (build) {
10370
- const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
10371
- 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` : ""}`);
10372
- }
10373
- const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
10374
- const harnessCols = column((r) => r.harness?.length ?? 0);
10375
- const modelCols = column((r) => r.model?.length ?? 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, usage(r))}`;
10387
- else if (r.state === "error")
10388
- detail = p(C.gray, usage(r));
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, ` │ ${ck.error ?? `exit ${ck.code}`}`));
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`)}`);
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
- ui.done({
11337
- runId,
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 cut(s, n) {
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 + cut(label, this.width - 9);
12068
- return `${head}${label.padEnd(28)}${this.paint("90", cut(status, this.width - 37))}`;
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 = cut(`${label.padEnd(cols)}${note}`, room);
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`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/firegoat-ai/firegoat-cli.git"