firegoat 0.12.0 → 0.14.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 +14 -1
- package/dist/cli.js +408 -86
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -88,6 +88,7 @@ npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
|
88
88
|
env:
|
|
89
89
|
ACME_HOME: "/tmp/acme-{{run.id}}-{{run.n}}" # sessions/config never collide across runs
|
|
90
90
|
ACME_TELEMETRY: "off"
|
|
91
|
+
UV_FIND_LINKS: "$FIREGOAT_OUT" # resolve the wheel setup built
|
|
91
92
|
tests:
|
|
92
93
|
- name: login
|
|
93
94
|
prompt: Log in to acme with the CLI and print the current user.
|
|
@@ -98,13 +99,15 @@ tests:
|
|
|
98
99
|
- run: ls "$ACME_HOME"/sessions | grep -q .
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
`env:` goes to the agent, and to `workdir_setup` and checks of the same run, so checks see the same values. Placeholders: `{{vars.KEY}}`, `{{run.id}}` (the eval's run id), `{{run.n}}` (1..runs), `{{
|
|
102
|
+
`env:` goes to the agent, and to `workdir_setup` and checks of the same run, so checks see the same values. Placeholders: `{{vars.KEY}}`, `{{run.id}}` (the eval's run id), `{{run.n}}` (1..runs), `{{test.name}}`, and `$FIREGOAT_OUT` / `$FIREGOAT_WORKDIR` (also `${...}`), the same dirs `setup`, `workdir_setup` and checks see. There is no other `$` expansion: `$HOME` stays the literal text `$HOME`. `setup` runs once, before any run: it gets only the top-level entries that use nothing but `{{vars.*}}`, `{{run.id}}` and `$FIREGOAT_OUT`. Values are strings (quote numbers). `PATH`, `CLAUDE_CONFIG_DIR` and `FIREGOAT_*` are firegoat's and can't be set.
|
|
102
103
|
|
|
103
104
|
## What eval does
|
|
104
105
|
|
|
105
106
|
- 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.
|
|
106
107
|
- 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`.
|
|
108
|
+
- In a linked git worktree (`git worktree add`), `.firegoat/runs/` and `.firegoat/.claude/` are in the main checkout, so runs (and their session files) survive `git worktree remove`; `firegoat.yaml`, fixtures and `setup:` stay in the worktree. Each `summary.json` records `dir` (where eval ran) and `git: {branch, commit, dirty}`, and `.firegoat/runs/index.jsonl` gets one line per eval (`runId, startedAt, dir, branch, commit, passed, failed, notGraded, cost`) to tell runs from different branches apart.
|
|
107
109
|
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, setup, workdir_setup, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it.
|
|
110
|
+
- Runs (tests × `runs`) go 4 at a time, each in its own folder with its own session; `--parallel N` changes that, `--parallel 1` runs them one after another. `summary.json` lists them in `firegoat.yaml` order.
|
|
108
111
|
- 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.
|
|
109
112
|
- 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.
|
|
110
113
|
|
|
@@ -119,6 +122,16 @@ Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See
|
|
|
119
122
|
| 2 | at least one run was not graded (claude missing or logged out, crash, timeout, setup failed); wins over 1 |
|
|
120
123
|
| 3 | usage or config error |
|
|
121
124
|
|
|
125
|
+
## Feedback
|
|
126
|
+
|
|
127
|
+
Something in firegoat got in your way (or your agent's)? Tell us from the terminal:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
npx firegoat feedback "eval hid claude's spend-limit error" --type bug --body "ran npx firegoat eval, got 'not graded' x3"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
It goes to the firegoat team with the firegoat version, your OS and node version, and which coding agent sent it. Don't include secrets: a report that looks like it has one is refused.
|
|
134
|
+
|
|
122
135
|
## Develop
|
|
123
136
|
|
|
124
137
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -6947,13 +6947,14 @@ 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.14.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
6954
6954
|
},
|
|
6955
6955
|
devDependencies: {
|
|
6956
6956
|
"@biomejs/biome": "^2.5.15",
|
|
6957
|
+
"@firegoat/feedback": "0.0.0",
|
|
6957
6958
|
"@types/bun": "latest",
|
|
6958
6959
|
typescript: "^7",
|
|
6959
6960
|
yaml: "^2.9.1"
|
|
@@ -7159,9 +7160,11 @@ var RESERVED_ENV = new Set([
|
|
|
7159
7160
|
"CLAUDE_CONFIG_DIR",
|
|
7160
7161
|
"CLAUDE_SECURESTORAGE_CONFIG_DIR"
|
|
7161
7162
|
]);
|
|
7162
|
-
var RUN_PLACEHOLDERS = ["run.id", "run.n", "
|
|
7163
|
+
var RUN_PLACEHOLDERS = ["run.id", "run.n", "test.name"];
|
|
7163
7164
|
var EVAL_PLACEHOLDERS = ["run.id"];
|
|
7164
7165
|
var ANY_PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g;
|
|
7166
|
+
var DIR_REF = /\$(?:\{(FIREGOAT_OUT|FIREGOAT_WORKDIR)\}|(FIREGOAT_OUT|FIREGOAT_WORKDIR)(?![A-Za-z0-9_]))/g;
|
|
7167
|
+
var ENV_REF = new RegExp(`${ANY_PLACEHOLDER.source}|${DIR_REF.source}`, "g");
|
|
7165
7168
|
function parseEnv(raw, path) {
|
|
7166
7169
|
if (raw === undefined)
|
|
7167
7170
|
return {};
|
|
@@ -7186,7 +7189,8 @@ function parseEnv(raw, path) {
|
|
|
7186
7189
|
return out;
|
|
7187
7190
|
}
|
|
7188
7191
|
function isEvalLevel(value) {
|
|
7189
|
-
|
|
7192
|
+
const dirsOk = [...value.matchAll(DIR_REF)].every((m) => (m[1] ?? m[2]) === "FIREGOAT_OUT");
|
|
7193
|
+
return dirsOk && [...value.matchAll(ANY_PLACEHOLDER)].every((m) => {
|
|
7190
7194
|
const name = m[1];
|
|
7191
7195
|
return name.startsWith("vars.") || EVAL_PLACEHOLDERS.includes(name);
|
|
7192
7196
|
});
|
|
@@ -7194,8 +7198,9 @@ function isEvalLevel(value) {
|
|
|
7194
7198
|
function renderEnv(env, vars, run) {
|
|
7195
7199
|
const out = {};
|
|
7196
7200
|
for (const [k, v] of Object.entries(env))
|
|
7197
|
-
out[k] = v.replace(
|
|
7198
|
-
const
|
|
7201
|
+
out[k] = v.replace(ENV_REF, (whole, name, braced, bare) => {
|
|
7202
|
+
const key = name ?? braced ?? bare;
|
|
7203
|
+
const filled = key.startsWith("vars.") ? vars[key.slice("vars.".length)] : run[key];
|
|
7199
7204
|
if (filled === undefined)
|
|
7200
7205
|
throw new ConfigError(`env.${k}: no value for ${whole}`);
|
|
7201
7206
|
return filled;
|
|
@@ -7224,13 +7229,14 @@ function loadConfig(dir) {
|
|
|
7224
7229
|
// src/eval.ts
|
|
7225
7230
|
import { randomBytes } from "node:crypto";
|
|
7226
7231
|
import {
|
|
7232
|
+
appendFileSync,
|
|
7227
7233
|
createWriteStream as createWriteStream2,
|
|
7228
7234
|
existsSync as existsSync2,
|
|
7229
7235
|
mkdirSync as mkdirSync2,
|
|
7230
7236
|
readdirSync as readdirSync2,
|
|
7231
7237
|
writeFileSync as writeFileSync3
|
|
7232
7238
|
} from "node:fs";
|
|
7233
|
-
import { basename, delimiter as delimiter2, join as join4 } from "node:path";
|
|
7239
|
+
import { basename as basename2, delimiter as delimiter2, join as join4 } from "node:path";
|
|
7234
7240
|
|
|
7235
7241
|
// src/claude.ts
|
|
7236
7242
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -7424,6 +7430,51 @@ function ensureFiregoatDir(root) {
|
|
|
7424
7430
|
return dir;
|
|
7425
7431
|
}
|
|
7426
7432
|
|
|
7433
|
+
// src/git.ts
|
|
7434
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7435
|
+
import { basename, dirname } from "node:path";
|
|
7436
|
+
function git(dir, args) {
|
|
7437
|
+
const r = spawnSync2("git", args, { cwd: dir, encoding: "utf8" });
|
|
7438
|
+
if (r.error)
|
|
7439
|
+
throw r.error;
|
|
7440
|
+
return { code: r.status ?? -1, stdout: r.stdout.trim() };
|
|
7441
|
+
}
|
|
7442
|
+
function gitRepo(dir) {
|
|
7443
|
+
const r = spawnSync2("git", ["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"], { cwd: dir, encoding: "utf8" });
|
|
7444
|
+
if (r.error?.code === "ENOENT")
|
|
7445
|
+
return null;
|
|
7446
|
+
if (r.error)
|
|
7447
|
+
throw r.error;
|
|
7448
|
+
if (r.status !== 0) {
|
|
7449
|
+
if (/not a git repository/.test(r.stderr))
|
|
7450
|
+
return null;
|
|
7451
|
+
throw new Error(`git rev-parse in ${dir} failed: ${r.stderr.trim()}`);
|
|
7452
|
+
}
|
|
7453
|
+
const [gitDir, commonDir] = r.stdout.trim().split(`
|
|
7454
|
+
`);
|
|
7455
|
+
if (!gitDir || !commonDir)
|
|
7456
|
+
throw new Error(`git rev-parse in ${dir} printed ${r.stdout}`);
|
|
7457
|
+
let mainCheckout = null;
|
|
7458
|
+
if (gitDir !== commonDir) {
|
|
7459
|
+
if (basename(commonDir) !== ".git")
|
|
7460
|
+
throw new Error(`${dir} is a linked git worktree, but its common git dir ${commonDir} is not a checkout's .git (a bare repo?), so there is no main checkout to keep .firegoat/runs in`);
|
|
7461
|
+
mainCheckout = dirname(commonDir);
|
|
7462
|
+
}
|
|
7463
|
+
const branch = git(dir, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
7464
|
+
const commit = git(dir, ["rev-parse", "-q", "--verify", "HEAD"]);
|
|
7465
|
+
const status = git(dir, ["status", "--porcelain"]);
|
|
7466
|
+
if (status.code !== 0)
|
|
7467
|
+
throw new Error(`git status in ${dir} failed`);
|
|
7468
|
+
return {
|
|
7469
|
+
info: {
|
|
7470
|
+
branch: branch.code === 0 ? branch.stdout : null,
|
|
7471
|
+
commit: commit.code === 0 ? commit.stdout : null,
|
|
7472
|
+
dirty: status.stdout !== ""
|
|
7473
|
+
},
|
|
7474
|
+
mainCheckout
|
|
7475
|
+
};
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7427
7478
|
// src/help.ts
|
|
7428
7479
|
var EXIT_CODES = `Exit codes:
|
|
7429
7480
|
0 every run's script checks passed
|
|
@@ -7438,6 +7489,7 @@ script checks.
|
|
|
7438
7489
|
|
|
7439
7490
|
npx firegoat init create firegoat.yaml and .firegoat/, install the skills
|
|
7440
7491
|
npx firegoat eval run the tests (claude must be logged in: run \`claude\` once)
|
|
7492
|
+
npx firegoat feedback "<what got in the way>" tell the firegoat team
|
|
7441
7493
|
|
|
7442
7494
|
Typical flow for a coding agent driving firegoat:
|
|
7443
7495
|
1. read npx firegoat init --help, then edit firegoat.yaml
|
|
@@ -7470,8 +7522,10 @@ firegoat.yaml reference:
|
|
|
7470
7522
|
env: {NAME: value} env vars for the agent, and for workdir_setup and
|
|
7471
7523
|
checks of the same run; values are strings and can
|
|
7472
7524
|
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
7473
|
-
{{
|
|
7474
|
-
|
|
7525
|
+
{{test.name}}, $FIREGOAT_OUT, $FIREGOAT_WORKDIR
|
|
7526
|
+
(no other $ expansion: $HOME stays literal).
|
|
7527
|
+
setup gets only the entries that use nothing but
|
|
7528
|
+
{{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
|
|
7475
7529
|
PATH, CLAUDE_CONFIG_DIR and FIREGOAT_* are reserved.
|
|
7476
7530
|
tests:
|
|
7477
7531
|
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
@@ -7512,8 +7566,8 @@ Example:
|
|
|
7512
7566
|
! grep -Eq '$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7513
7567
|
|
|
7514
7568
|
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7515
|
-
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value]..
|
|
7516
|
-
[--claude-bin PATH]
|
|
7569
|
+
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--parallel N] [--var KEY=value]..
|
|
7570
|
+
[--json] [--claude-bin PATH]
|
|
7517
7571
|
|
|
7518
7572
|
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
7519
7573
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with setup:'s
|
|
@@ -7524,15 +7578,23 @@ with a fresh config dir, .firegoat/.claude: none of your settings, CLAUDE.md,
|
|
|
7524
7578
|
plugins, skills or MCP servers. claude saves its session there. There is no
|
|
7525
7579
|
sandbox: the agent can reach the rest of your machine, including the repo.
|
|
7526
7580
|
|
|
7581
|
+
In a linked git worktree (git worktree add), .firegoat/runs/ and .firegoat/.claude
|
|
7582
|
+
live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
|
|
7583
|
+
fixtures and setup: stay in the current folder.
|
|
7584
|
+
|
|
7527
7585
|
test.. only these tests (by name)
|
|
7528
7586
|
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7587
|
+
--parallel N runs at once (default 4; 1 runs them one after another)
|
|
7529
7588
|
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7530
7589
|
--json the summary as JSON on stdout (or {error, exitCode} if eval
|
|
7531
7590
|
stops early); progress goes to stderr
|
|
7532
7591
|
--claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
|
|
7533
7592
|
|
|
7534
7593
|
Output: .firegoat/runs/<runId>/
|
|
7535
|
-
summary.json every run, plus totals
|
|
7594
|
+
summary.json every run, plus totals, dir (where eval ran) and
|
|
7595
|
+
git {branch, commit, dirty} (no git outside a repo)
|
|
7596
|
+
../index.jsonl one line per eval: runId, startedAt, dir, branch,
|
|
7597
|
+
commit, passed, failed, notGraded, cost
|
|
7536
7598
|
<test>/<n>/run.json one run: status and reason (from the checks), prompt,
|
|
7537
7599
|
end_goal, checks [{run, code}], and paths: workdir,
|
|
7538
7600
|
transcript, session (claude's session file), stderr,
|
|
@@ -7553,6 +7615,25 @@ Examples:
|
|
|
7553
7615
|
A run can take minutes (default timeout 600s per test).
|
|
7554
7616
|
|
|
7555
7617
|
${EXIT_CODES}`;
|
|
7618
|
+
var FEEDBACK_HELP = `Usage: firegoat feedback "<title>" [--body TEXT|-] [--type bug|feature_request|feedback]
|
|
7619
|
+
|
|
7620
|
+
Sends feedback about firegoat to the firegoat team (firegoat.ai). Coding agents: if
|
|
7621
|
+
firegoat confused you, failed you or cost you tokens, say so here: one concrete
|
|
7622
|
+
title, then what you ran and what happened in --body (- reads stdin).
|
|
7623
|
+
|
|
7624
|
+
"<title>" 8-200 characters, e.g. "eval hid claude's spend-limit error"
|
|
7625
|
+
--body TEXT details: commands, output, what you expected
|
|
7626
|
+
--type bug, feature_request or feedback (default)
|
|
7627
|
+
|
|
7628
|
+
Sent along: the firegoat version, your OS and node version, and which agent you are
|
|
7629
|
+
(claude-code/codex/cursor, from the environment). Don't include secrets: a report that
|
|
7630
|
+
looks like it contains one is refused.
|
|
7631
|
+
|
|
7632
|
+
Example:
|
|
7633
|
+
npx firegoat feedback "checks can't grep commands with quotes" --type bug \\
|
|
7634
|
+
--body "grep '"command":"...' stops at the first escaped quote"
|
|
7635
|
+
|
|
7636
|
+
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
7556
7637
|
|
|
7557
7638
|
// src/setup.ts
|
|
7558
7639
|
import { createWriteStream } from "node:fs";
|
|
@@ -7670,10 +7751,14 @@ ${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
|
7670
7751
|
}
|
|
7671
7752
|
|
|
7672
7753
|
// src/ui.ts
|
|
7673
|
-
import { relative as relative2 } from "node:path";
|
|
7754
|
+
import { isAbsolute, relative as relative2 } from "node:path";
|
|
7674
7755
|
var RAMP = "▁▂▃▄▅▆▇";
|
|
7675
7756
|
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7676
7757
|
var SPIN = ["◐", "◓", "◑", "◒"];
|
|
7758
|
+
function shortPath(path) {
|
|
7759
|
+
const rel = relative2(process.cwd(), path);
|
|
7760
|
+
return rel.startsWith("..") || isAbsolute(rel) ? path : rel;
|
|
7761
|
+
}
|
|
7677
7762
|
function painter(color) {
|
|
7678
7763
|
return (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
7679
7764
|
}
|
|
@@ -7686,6 +7771,17 @@ var C = {
|
|
|
7686
7771
|
bold: "1",
|
|
7687
7772
|
cyan: "36"
|
|
7688
7773
|
};
|
|
7774
|
+
var BUTTERFLY = [" \\ /", " (_\\/_)", " (_/\\_)"];
|
|
7775
|
+
var LOGO_COLS = 10;
|
|
7776
|
+
function logoHeader(paint, title, right, width) {
|
|
7777
|
+
return BUTTERFLY.map((wing, i) => {
|
|
7778
|
+
const art = paint(C.ember, wing);
|
|
7779
|
+
if (i !== 1)
|
|
7780
|
+
return art;
|
|
7781
|
+
const used = LOGO_COLS + title.length + right.replace(ANSI, "").length;
|
|
7782
|
+
return `${art}${" ".repeat(LOGO_COLS - wing.length)}${paint(C.bold, title)}${" ".repeat(Math.max(2, width - used - 1))}${right}`;
|
|
7783
|
+
});
|
|
7784
|
+
}
|
|
7689
7785
|
function tier(rows) {
|
|
7690
7786
|
const graded = rows.filter((r) => r.state === "pass" || r.state === "fail");
|
|
7691
7787
|
if (graded.length === 0)
|
|
@@ -7720,9 +7816,10 @@ function checksPassed(r) {
|
|
|
7720
7816
|
function total(rows) {
|
|
7721
7817
|
const cost = rows.reduce((s, r) => s + (r.cost ?? 0), 0);
|
|
7722
7818
|
const turns = rows.reduce((s, r) => s + r.turns, 0);
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7819
|
+
return { cost, turns };
|
|
7820
|
+
}
|
|
7821
|
+
function duration(seconds) {
|
|
7822
|
+
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
7726
7823
|
}
|
|
7727
7824
|
function counts(rows) {
|
|
7728
7825
|
return {
|
|
@@ -7809,9 +7906,8 @@ function ttyUI(out, opts) {
|
|
|
7809
7906
|
const c = counts(rows);
|
|
7810
7907
|
const doneCount = c.pass + c.fail + c.error;
|
|
7811
7908
|
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
7812
|
-
const title = ` \uD83E\uDD8B firegoat eval · ${opts.project}`;
|
|
7813
7909
|
const out = [
|
|
7814
|
-
|
|
7910
|
+
...logoHeader(p, `firegoat eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
|
|
7815
7911
|
rule
|
|
7816
7912
|
];
|
|
7817
7913
|
if (authText)
|
|
@@ -7847,22 +7943,23 @@ function ttyUI(out, opts) {
|
|
|
7847
7943
|
if (!finished) {
|
|
7848
7944
|
const track = Math.max(10, w - 34);
|
|
7849
7945
|
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
7850
|
-
const
|
|
7851
|
-
out.push(` ${p(C.ember,
|
|
7946
|
+
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
7947
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · $${t.cost.toFixed(2)} so far`)}`);
|
|
7852
7948
|
return out;
|
|
7853
7949
|
}
|
|
7854
7950
|
const tr = tier(rows);
|
|
7855
7951
|
const goated = c.pass === rows.length && rows.length > 0;
|
|
7856
7952
|
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
7857
|
-
const
|
|
7858
|
-
|
|
7859
|
-
out.push(p(C.
|
|
7860
|
-
out.push(p(C.gray, `
|
|
7953
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
7954
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `tier ${tr}` : "";
|
|
7955
|
+
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
7956
|
+
out.push(p(C.gray, ` total $${t.cost.toFixed(2)} · ${t.turns} turns · ${duration(finished.seconds)}`));
|
|
7957
|
+
out.push(p(C.gray, ` runs ${shortPath(finished.runsDir)}/`));
|
|
7861
7958
|
out.push(` checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
7862
7959
|
return out;
|
|
7863
7960
|
};
|
|
7864
7961
|
const rowsOf = (line) => {
|
|
7865
|
-
const visible = line.replace(ANSI, "")
|
|
7962
|
+
const visible = line.replace(ANSI, "");
|
|
7866
7963
|
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
7867
7964
|
};
|
|
7868
7965
|
const render = () => {
|
|
@@ -7892,9 +7989,9 @@ function ttyUI(out, opts) {
|
|
|
7892
7989
|
render();
|
|
7893
7990
|
},
|
|
7894
7991
|
update: render,
|
|
7895
|
-
done: ({ runId, runsDir }) => {
|
|
7992
|
+
done: ({ runId, runsDir, seconds }) => {
|
|
7896
7993
|
clearInterval(timer);
|
|
7897
|
-
finished = { runId, runsDir };
|
|
7994
|
+
finished = { runId, runsDir, seconds };
|
|
7898
7995
|
render();
|
|
7899
7996
|
}
|
|
7900
7997
|
};
|
|
@@ -7929,6 +8026,7 @@ class UsageError extends Error {
|
|
|
7929
8026
|
function parseArgs(args) {
|
|
7930
8027
|
const opts = {
|
|
7931
8028
|
names: [],
|
|
8029
|
+
parallel: 4,
|
|
7932
8030
|
json: false,
|
|
7933
8031
|
vars: {},
|
|
7934
8032
|
claudeBin: process.env.FIREGOAT_CLAUDE_BIN || "claude"
|
|
@@ -7961,6 +8059,12 @@ function parseArgs(args) {
|
|
|
7961
8059
|
if (!/^\d+$/.test(v) || n < 1)
|
|
7962
8060
|
throw new UsageError(`--runs must be a positive integer, got ${v}`);
|
|
7963
8061
|
opts.runs = n;
|
|
8062
|
+
} else if (flag === "--parallel") {
|
|
8063
|
+
const v = value();
|
|
8064
|
+
const n = Number(v);
|
|
8065
|
+
if (!/^\d+$/.test(v) || n < 1)
|
|
8066
|
+
throw new UsageError(`--parallel must be a positive integer, got ${v}`);
|
|
8067
|
+
opts.parallel = n;
|
|
7964
8068
|
} else if (flag === "--claude-bin")
|
|
7965
8069
|
opts.claudeBin = value();
|
|
7966
8070
|
else if (a.startsWith("-"))
|
|
@@ -8020,8 +8124,9 @@ async function runOne(opts) {
|
|
|
8020
8124
|
const env = renderEnv(test.env, opts.vars, {
|
|
8021
8125
|
"run.id": opts.runId,
|
|
8022
8126
|
"run.n": String(n),
|
|
8023
|
-
"
|
|
8024
|
-
|
|
8127
|
+
"test.name": test.name,
|
|
8128
|
+
FIREGOAT_OUT: opts.out,
|
|
8129
|
+
FIREGOAT_WORKDIR: workdir
|
|
8025
8130
|
});
|
|
8026
8131
|
const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
|
|
8027
8132
|
if (opts.workdirSetup.length > 0) {
|
|
@@ -8105,7 +8210,7 @@ async function evalCommand(args, root) {
|
|
|
8105
8210
|
}
|
|
8106
8211
|
const say = opts.json ? console.error : console.log;
|
|
8107
8212
|
const stream = opts.json ? process.stderr : process.stdout;
|
|
8108
|
-
const ui = stream.isTTY ? ttyUI(stream, { project:
|
|
8213
|
+
const ui = stream.isTTY ? ttyUI(stream, { project: basename2(root), color: !process.env.NO_COLOR }) : plainUI(say);
|
|
8109
8214
|
let config;
|
|
8110
8215
|
try {
|
|
8111
8216
|
config = loadConfig(root);
|
|
@@ -8136,7 +8241,13 @@ async function evalCommand(args, root) {
|
|
|
8136
8241
|
const claudePath = resolveBin(opts.claudeBin);
|
|
8137
8242
|
if (!claudePath)
|
|
8138
8243
|
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.`);
|
|
8139
|
-
|
|
8244
|
+
let repo;
|
|
8245
|
+
try {
|
|
8246
|
+
repo = gitRepo(root);
|
|
8247
|
+
} catch (e) {
|
|
8248
|
+
return fatal(EXIT.usage, e.message);
|
|
8249
|
+
}
|
|
8250
|
+
const firegoatDir = ensureFiregoatDir(repo?.mainCheckout ?? root);
|
|
8140
8251
|
const configDir = join4(firegoatDir, ".claude");
|
|
8141
8252
|
mkdirSync2(configDir, { recursive: true });
|
|
8142
8253
|
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");
|
|
@@ -8147,8 +8258,10 @@ async function evalCommand(args, root) {
|
|
|
8147
8258
|
});
|
|
8148
8259
|
if (loginProblem)
|
|
8149
8260
|
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
8261
|
+
const evalStarted = Date.now();
|
|
8150
8262
|
const runId = newRunId();
|
|
8151
|
-
const
|
|
8263
|
+
const runsRoot = join4(firegoatDir, "runs");
|
|
8264
|
+
const runDir = join4(runsRoot, runId);
|
|
8152
8265
|
const out = join4(runDir, "out");
|
|
8153
8266
|
mkdirSync2(out, { recursive: true });
|
|
8154
8267
|
const stepEnv = {
|
|
@@ -8172,7 +8285,7 @@ async function evalCommand(args, root) {
|
|
|
8172
8285
|
cwd: root,
|
|
8173
8286
|
env: {
|
|
8174
8287
|
...stepEnv,
|
|
8175
|
-
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId })
|
|
8288
|
+
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
|
|
8176
8289
|
},
|
|
8177
8290
|
logPath: join4(runDir, "setup.txt")
|
|
8178
8291
|
});
|
|
@@ -8182,65 +8295,271 @@ async function evalCommand(args, root) {
|
|
|
8182
8295
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
8183
8296
|
}
|
|
8184
8297
|
ui.runs(rows);
|
|
8185
|
-
const results =
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8298
|
+
const results = new Array(rows.length);
|
|
8299
|
+
const runAt = async (i) => {
|
|
8300
|
+
const row = rows[i];
|
|
8301
|
+
const test = tests.find((t) => t.name === row.test);
|
|
8302
|
+
row.state = "running";
|
|
8303
|
+
ui.update();
|
|
8304
|
+
const r = await runOne({
|
|
8305
|
+
onEvent: (e) => {
|
|
8306
|
+
if (e.type !== "assistant")
|
|
8307
|
+
return;
|
|
8308
|
+
row.turns++;
|
|
8309
|
+
const content = e.message?.content;
|
|
8310
|
+
if (!Array.isArray(content))
|
|
8311
|
+
return;
|
|
8312
|
+
for (const b of content) {
|
|
8313
|
+
const cmd = b.input?.command;
|
|
8314
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
8315
|
+
row.command = cmd.split(`
|
|
8203
8316
|
`)[0];
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8317
|
+
}
|
|
8318
|
+
},
|
|
8319
|
+
test,
|
|
8320
|
+
n: row.n,
|
|
8321
|
+
runId,
|
|
8322
|
+
runDir,
|
|
8323
|
+
claudeBin: claudePath,
|
|
8324
|
+
configDir,
|
|
8325
|
+
workdirSetup: config.workdirSetup,
|
|
8326
|
+
out,
|
|
8327
|
+
stepEnv,
|
|
8328
|
+
vars
|
|
8329
|
+
});
|
|
8330
|
+
results[i] = r;
|
|
8331
|
+
Object.assign(row, {
|
|
8332
|
+
state: r.status,
|
|
8333
|
+
turns: r.num_turns ?? row.turns,
|
|
8334
|
+
reason: r.reason,
|
|
8335
|
+
checks: r.checks,
|
|
8336
|
+
cost: r.cost,
|
|
8337
|
+
seconds: Math.round(r.duration_ms / 1000),
|
|
8338
|
+
command: undefined
|
|
8339
|
+
});
|
|
8340
|
+
ui.update();
|
|
8341
|
+
};
|
|
8342
|
+
let next = 0;
|
|
8343
|
+
const worker = async () => {
|
|
8344
|
+
while (next < rows.length)
|
|
8345
|
+
await runAt(next++);
|
|
8346
|
+
};
|
|
8347
|
+
await Promise.all(Array.from({ length: Math.min(opts.parallel, rows.length) }, worker));
|
|
8228
8348
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
8229
8349
|
for (const r of results)
|
|
8230
8350
|
totals[r.status]++;
|
|
8231
|
-
const doc = {
|
|
8351
|
+
const doc = {
|
|
8352
|
+
runId,
|
|
8353
|
+
runsDir: runDir,
|
|
8354
|
+
dir: root,
|
|
8355
|
+
...repo && { git: repo.info },
|
|
8356
|
+
totals,
|
|
8357
|
+
runs: results
|
|
8358
|
+
};
|
|
8232
8359
|
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
8233
8360
|
`);
|
|
8234
|
-
|
|
8361
|
+
appendFileSync(join4(runsRoot, "index.jsonl"), `${JSON.stringify({
|
|
8362
|
+
runId,
|
|
8363
|
+
startedAt: new Date(evalStarted).toISOString(),
|
|
8364
|
+
dir: root,
|
|
8365
|
+
branch: repo?.info.branch ?? null,
|
|
8366
|
+
commit: repo?.info.commit ?? null,
|
|
8367
|
+
passed: totals.pass,
|
|
8368
|
+
failed: totals.fail,
|
|
8369
|
+
notGraded: totals.error,
|
|
8370
|
+
cost: results.reduce((sum, r) => sum + (r.cost ?? 0), 0)
|
|
8371
|
+
})}
|
|
8372
|
+
`);
|
|
8373
|
+
ui.done({
|
|
8374
|
+
runId,
|
|
8375
|
+
runsDir: runDir,
|
|
8376
|
+
rows,
|
|
8377
|
+
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
8378
|
+
});
|
|
8235
8379
|
if (opts.json)
|
|
8236
8380
|
console.log(JSON.stringify(doc, null, 2));
|
|
8237
8381
|
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
8238
8382
|
}
|
|
8239
8383
|
|
|
8384
|
+
// src/feedback.ts
|
|
8385
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8386
|
+
|
|
8387
|
+
// node_modules/@firegoat/feedback/dist/index.js
|
|
8388
|
+
var VERSION = "0.0.0";
|
|
8389
|
+
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
8390
|
+
var TIMEOUT_MS = 15000;
|
|
8391
|
+
function envBaseUrl() {
|
|
8392
|
+
return globalThis.process?.env?.FEEDBACK_API_URL;
|
|
8393
|
+
}
|
|
8394
|
+
|
|
8395
|
+
class FeedbackError extends Error {
|
|
8396
|
+
status;
|
|
8397
|
+
code;
|
|
8398
|
+
retryAfterSeconds;
|
|
8399
|
+
detected;
|
|
8400
|
+
constructor(status, code, message, retryAfterSeconds, detected) {
|
|
8401
|
+
super(message);
|
|
8402
|
+
this.status = status;
|
|
8403
|
+
this.code = code;
|
|
8404
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
8405
|
+
this.detected = detected;
|
|
8406
|
+
this.name = "FeedbackError";
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
var TITLE_MIN = 8;
|
|
8410
|
+
var TITLE_MAX = 200;
|
|
8411
|
+
|
|
8412
|
+
class FeedbackClient {
|
|
8413
|
+
domain;
|
|
8414
|
+
baseUrl;
|
|
8415
|
+
token;
|
|
8416
|
+
fetchImpl;
|
|
8417
|
+
constructor(options) {
|
|
8418
|
+
if (!options.domain?.trim()) {
|
|
8419
|
+
throw new Error("FeedbackClient: `domain` is required (your product's domain, e.g. example.com)");
|
|
8420
|
+
}
|
|
8421
|
+
this.domain = options.domain.trim();
|
|
8422
|
+
this.baseUrl = (options.baseUrl ?? envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
8423
|
+
this.token = options.token;
|
|
8424
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
8425
|
+
}
|
|
8426
|
+
async registerDevice() {
|
|
8427
|
+
const data = await this.post("/api/feedback/device", undefined);
|
|
8428
|
+
if (typeof data.token !== "string" || !data.token) {
|
|
8429
|
+
throw new FeedbackError(201, "invalid_response", "server response missing token");
|
|
8430
|
+
}
|
|
8431
|
+
this.token = data.token;
|
|
8432
|
+
return data.token;
|
|
8433
|
+
}
|
|
8434
|
+
async send(input) {
|
|
8435
|
+
const title = input.title.trim();
|
|
8436
|
+
if (title.length < TITLE_MIN || title.length > TITLE_MAX) {
|
|
8437
|
+
throw new FeedbackError(400, "invalid_request", `title must be ${TITLE_MIN}-${TITLE_MAX} characters, got ${title.length}`);
|
|
8438
|
+
}
|
|
8439
|
+
const meta = {
|
|
8440
|
+
...input.meta,
|
|
8441
|
+
cliVersion: `@firegoat/feedback@${VERSION}`,
|
|
8442
|
+
feedbackType: input.type
|
|
8443
|
+
};
|
|
8444
|
+
const data = await this.post("/api/feedback", {
|
|
8445
|
+
domain: this.domain,
|
|
8446
|
+
title,
|
|
8447
|
+
body: input.body ?? "",
|
|
8448
|
+
meta,
|
|
8449
|
+
maskSecrets: input.maskSecrets ?? false
|
|
8450
|
+
});
|
|
8451
|
+
if (typeof data.id !== "string" || !data.id) {
|
|
8452
|
+
throw new FeedbackError(201, "invalid_response", "server response missing id");
|
|
8453
|
+
}
|
|
8454
|
+
return { id: data.id, similarCount: data.similarCount ?? 0 };
|
|
8455
|
+
}
|
|
8456
|
+
async post(path, body) {
|
|
8457
|
+
const headers = {
|
|
8458
|
+
"content-type": "application/json"
|
|
8459
|
+
};
|
|
8460
|
+
if (this.token)
|
|
8461
|
+
headers["x-feedback-token"] = this.token;
|
|
8462
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
8463
|
+
method: "POST",
|
|
8464
|
+
headers,
|
|
8465
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
8466
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8467
|
+
});
|
|
8468
|
+
const text = await res.text();
|
|
8469
|
+
let data;
|
|
8470
|
+
try {
|
|
8471
|
+
data = JSON.parse(text);
|
|
8472
|
+
} catch {
|
|
8473
|
+
throw new FeedbackError(res.status, "invalid_response", `non-JSON response (${res.status}): ${text.slice(0, 200)}`);
|
|
8474
|
+
}
|
|
8475
|
+
if (!res.ok) {
|
|
8476
|
+
throw new FeedbackError(res.status, String(data.error ?? "unknown_error"), String(data.message ?? `request failed with status ${res.status}`), typeof data.retryAfterSeconds === "number" ? data.retryAfterSeconds : undefined, Array.isArray(data.detected) ? data.detected : undefined);
|
|
8477
|
+
}
|
|
8478
|
+
return data;
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
|
|
8482
|
+
// src/feedback.ts
|
|
8483
|
+
var TYPES = ["bug", "feature_request", "feedback"];
|
|
8484
|
+
function agentName() {
|
|
8485
|
+
if (process.env.CLAUDECODE)
|
|
8486
|
+
return "claude-code";
|
|
8487
|
+
if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX)
|
|
8488
|
+
return "codex";
|
|
8489
|
+
if (process.env.CURSOR_TRACE_ID)
|
|
8490
|
+
return "cursor";
|
|
8491
|
+
return;
|
|
8492
|
+
}
|
|
8493
|
+
async function feedbackCommand(args) {
|
|
8494
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
8495
|
+
console.log(FEEDBACK_HELP);
|
|
8496
|
+
return args.length === 0 ? EXIT.usage : EXIT.pass;
|
|
8497
|
+
}
|
|
8498
|
+
const titleParts = [];
|
|
8499
|
+
let body;
|
|
8500
|
+
let type = "feedback";
|
|
8501
|
+
for (let i = 0;i < args.length; i++) {
|
|
8502
|
+
const a = args[i];
|
|
8503
|
+
const value = () => {
|
|
8504
|
+
const v = args[++i];
|
|
8505
|
+
if (v === undefined)
|
|
8506
|
+
throw new Error(`${a} needs a value`);
|
|
8507
|
+
return v;
|
|
8508
|
+
};
|
|
8509
|
+
try {
|
|
8510
|
+
if (a === "--body") {
|
|
8511
|
+
const v = value();
|
|
8512
|
+
body = v === "-" ? readFileSync2(0, "utf8") : v;
|
|
8513
|
+
} else if (a === "--type") {
|
|
8514
|
+
const v = value();
|
|
8515
|
+
if (!TYPES.includes(v))
|
|
8516
|
+
throw new Error(`--type must be one of ${TYPES.join(", ")}, got ${v}`);
|
|
8517
|
+
type = v;
|
|
8518
|
+
} else if (a.startsWith("-"))
|
|
8519
|
+
throw new Error(`unknown flag: ${a}`);
|
|
8520
|
+
else
|
|
8521
|
+
titleParts.push(a);
|
|
8522
|
+
} catch (e) {
|
|
8523
|
+
console.error(`firegoat feedback: ${e.message}`);
|
|
8524
|
+
return EXIT.usage;
|
|
8525
|
+
}
|
|
8526
|
+
}
|
|
8527
|
+
const title = titleParts.join(" ").trim();
|
|
8528
|
+
if (title.length < 8 || title.length > 200) {
|
|
8529
|
+
console.error(`firegoat feedback: the title must be 8-200 characters (got ${title.length})`);
|
|
8530
|
+
return EXIT.usage;
|
|
8531
|
+
}
|
|
8532
|
+
const client = new FeedbackClient({ domain: "firegoat.ai" });
|
|
8533
|
+
try {
|
|
8534
|
+
const agent = agentName();
|
|
8535
|
+
const { id, similarCount } = await client.send({
|
|
8536
|
+
title,
|
|
8537
|
+
body: body?.trim() ? `${body.trim()}
|
|
8538
|
+
|
|
8539
|
+
firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
8540
|
+
type,
|
|
8541
|
+
meta: {
|
|
8542
|
+
...agent ? { agent } : {},
|
|
8543
|
+
os: process.platform,
|
|
8544
|
+
nodeVersion: process.version
|
|
8545
|
+
}
|
|
8546
|
+
});
|
|
8547
|
+
console.log(`sent ${type} ${id} to the firegoat team${similarCount > 0 ? ` (${similarCount} similar report${similarCount === 1 ? "" : "s"} already)` : ""}. thanks.`);
|
|
8548
|
+
return EXIT.pass;
|
|
8549
|
+
} catch (e) {
|
|
8550
|
+
if (e instanceof FeedbackError) {
|
|
8551
|
+
const hint = e.code === "possible_secret" ? " Remove the secret from the title/body and send again." : e.code === "rate_limited" && e.retryAfterSeconds ? ` Try again in ${e.retryAfterSeconds}s.` : "";
|
|
8552
|
+
console.error(`firegoat feedback: ${e.code}: ${e.message}.${hint}`);
|
|
8553
|
+
return e.code === "invalid_request" || e.code === "possible_secret" ? EXIT.usage : EXIT.notGraded;
|
|
8554
|
+
}
|
|
8555
|
+
throw e;
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
|
|
8240
8559
|
// src/init.ts
|
|
8241
|
-
import { spawnSync as
|
|
8560
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
8242
8561
|
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8243
|
-
import { basename as
|
|
8562
|
+
import { basename as basename3, join as join5 } from "node:path";
|
|
8244
8563
|
import { createInterface } from "node:readline/promises";
|
|
8245
8564
|
var SKILLS_ARGV = [
|
|
8246
8565
|
"skills",
|
|
@@ -8286,9 +8605,11 @@ runs: 1
|
|
|
8286
8605
|
|
|
8287
8606
|
# Optional: env vars for the agent (and for workdir_setup and checks of the same
|
|
8288
8607
|
# run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
|
|
8289
|
-
# {{run.id}}, {{run.n}}, {{
|
|
8608
|
+
# {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR (no
|
|
8609
|
+
# other $ expansion: $HOME stays literal). A test can add its own env:.
|
|
8290
8610
|
# env:
|
|
8291
8611
|
# MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{run.n}}"
|
|
8612
|
+
# UV_FIND_LINKS: "$FIREGOAT_OUT"
|
|
8292
8613
|
|
|
8293
8614
|
tests:
|
|
8294
8615
|
- name: quickstart
|
|
@@ -8317,7 +8638,7 @@ var CLIPBOARD = [
|
|
|
8317
8638
|
];
|
|
8318
8639
|
function copyToClipboard(text) {
|
|
8319
8640
|
return CLIPBOARD.some(([cmd, args]) => {
|
|
8320
|
-
const r =
|
|
8641
|
+
const r = spawnSync3(cmd, args, { input: text });
|
|
8321
8642
|
return !r.error && r.status === 0;
|
|
8322
8643
|
});
|
|
8323
8644
|
}
|
|
@@ -8356,16 +8677,15 @@ ${INIT_HELP}`);
|
|
|
8356
8677
|
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
8357
8678
|
const step = (n, label, status) => console.log(` ${paint("32", "●")} ${paint("90", String(n).padStart(2, "0"))} ${label.padEnd(28)}${paint("90", status)}`);
|
|
8358
8679
|
if (interactive) {
|
|
8359
|
-
const
|
|
8360
|
-
|
|
8361
|
-
console.log(`${paint("38;5;208", " \uD83E\uDD8B")} firegoat init${" ".repeat(Math.max(2, width - title.length - tag.length - 2))}${paint("90", tag)}`);
|
|
8680
|
+
for (const line of logoHeader(paint, "firegoat init", paint("90", "Get agent-ready."), width))
|
|
8681
|
+
console.log(line);
|
|
8362
8682
|
console.log(rule);
|
|
8363
8683
|
}
|
|
8364
8684
|
ensureFiregoatDir(cwd);
|
|
8365
8685
|
const configPath = join5(cwd, CONFIG_FILE);
|
|
8366
8686
|
const keep = existsSync3(configPath) && !force;
|
|
8367
8687
|
if (!keep)
|
|
8368
|
-
writeFileSync4(configPath, template(
|
|
8688
|
+
writeFileSync4(configPath, template(basename3(cwd)));
|
|
8369
8689
|
if (interactive) {
|
|
8370
8690
|
step(1, CONFIG_FILE, keep ? "kept (--force replaces it)" : "created");
|
|
8371
8691
|
console.log(paint("90", " │"));
|
|
@@ -8382,7 +8702,7 @@ ${INIT_HELP}`);
|
|
|
8382
8702
|
console.log(`installing skills: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8383
8703
|
const env = { ...process.env };
|
|
8384
8704
|
delete env.npm_config_package;
|
|
8385
|
-
const r =
|
|
8705
|
+
const r = spawnSync3("npx", ["-y", ...SKILLS_ARGV], {
|
|
8386
8706
|
cwd,
|
|
8387
8707
|
env,
|
|
8388
8708
|
stdio: "pipe",
|
|
@@ -8397,7 +8717,7 @@ ${INIT_HELP}`);
|
|
|
8397
8717
|
step(3, "firegoat-onboarding skill", "installed → .claude/skills/");
|
|
8398
8718
|
else
|
|
8399
8719
|
console.log("installed skills firegoat-onboarding, suggesting-eval-prompts in .claude/skills/");
|
|
8400
|
-
const ignored =
|
|
8720
|
+
const ignored = spawnSync3("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
8401
8721
|
cwd
|
|
8402
8722
|
});
|
|
8403
8723
|
if (ignored.status === 0)
|
|
@@ -8436,6 +8756,8 @@ async function main(argv) {
|
|
|
8436
8756
|
return initCommand(rest, process.cwd());
|
|
8437
8757
|
case "eval":
|
|
8438
8758
|
return evalCommand(rest, process.cwd());
|
|
8759
|
+
case "feedback":
|
|
8760
|
+
return feedbackCommand(rest);
|
|
8439
8761
|
default:
|
|
8440
8762
|
console.error(`unknown command: ${cmd}
|
|
8441
8763
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firegoat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
7
7
|
},
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"@biomejs/biome": "^2.5.15",
|
|
10
|
+
"@firegoat/feedback": "0.0.0",
|
|
10
11
|
"@types/bun": "latest",
|
|
11
12
|
"typescript": "^7",
|
|
12
13
|
"yaml": "^2.9.1"
|