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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +339 -43
  3. 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.10). See `plan.md`.
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.10.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
- stdout += c.toString();
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
- say(process.env.ANTHROPIC_API_KEY ? "auth: ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "auth: your claude login");
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
- say("setup...");
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
- say(`${r.status.toUpperCase()} ${r.test}#${r.n} ${r.reason} (${usage(r)})`);
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
- const cost = results.reduce((sum, r) => sum + (r.cost ?? 0), 0);
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
- if (existsSync3(configPath) && !force)
7989
- console.log(`kept ${CONFIG_FILE} (--force replaces it)`);
7990
- else {
7991
- writeFileSync4(configPath, template(basename(cwd)));
7992
- console.log(`wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
7993
- }
7994
- const interactive = process.stdin.isTTY && process.stdout.isTTY;
7995
- if (skills && interactive)
7996
- skills = await askYes("Install the firegoat-onboarding skill for your coding agent?");
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
- console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
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: interactive ? "inherit" : "pipe",
8293
+ stdio: "pipe",
8005
8294
  encoding: "utf8"
8006
8295
  });
8007
8296
  if (r.error || r.status !== 0) {
8008
- if (!interactive)
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
- console.log("installed skills firegoat-onboarding, suggesting-eval-prompts in .claude/skills/");
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
- console.log(interactive ? `
8021
- Paste this into your coding agent:
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
- if (interactive && copyToClipboard(AGENT_PROMPT))
8027
- console.log("\x1B[32mPrompt copied!\x1B[0m");
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/firegoat-ai/firegoat-cli.git"