firegoat 0.11.0 → 0.13.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 +31 -1
- package/dist/cli.js +395 -71
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -82,14 +82,34 @@ tests:
|
|
|
82
82
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Env vars for the agent: a home dir per run, a feature flag
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
env:
|
|
89
|
+
ACME_HOME: "/tmp/acme-{{run.id}}-{{run.n}}" # sessions/config never collide across runs
|
|
90
|
+
ACME_TELEMETRY: "off"
|
|
91
|
+
tests:
|
|
92
|
+
- name: login
|
|
93
|
+
prompt: Log in to acme with the CLI and print the current user.
|
|
94
|
+
end_goal: The agent completes acme's login flow and prints the user.
|
|
95
|
+
env:
|
|
96
|
+
ACME_BETA_LOGIN: "1" # this test only, on top of env:
|
|
97
|
+
checks:
|
|
98
|
+
- run: ls "$ACME_HOME"/sessions | grep -q .
|
|
99
|
+
```
|
|
100
|
+
|
|
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), `{{run.workdir}}`, `{{test.name}}`. `setup` runs once, before any run: it gets only the top-level entries that use nothing but `{{vars.*}}` and `{{run.id}}`. Values are strings (quote numbers). `PATH`, `CLAUDE_CONFIG_DIR` and `FIREGOAT_*` are firegoat's and can't be set.
|
|
102
|
+
|
|
85
103
|
## What eval does
|
|
86
104
|
|
|
87
105
|
- 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.
|
|
88
106
|
- 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`.
|
|
107
|
+
- 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.
|
|
108
|
+
- 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.
|
|
89
109
|
- 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.
|
|
90
110
|
- 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.
|
|
91
111
|
|
|
92
|
-
`firegoat.yaml` keys: `runs`, `setup`, `workdir_setup`, `vars`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
112
|
+
`firegoat.yaml` keys: `runs`, `setup`, `workdir_setup`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
93
113
|
|
|
94
114
|
Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See [`examples/local-npm-registry`](examples/local-npm-registry/README.md).
|
|
95
115
|
|
|
@@ -100,6 +120,16 @@ Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See
|
|
|
100
120
|
| 2 | at least one run was not graded (claude missing or logged out, crash, timeout, setup failed); wins over 1 |
|
|
101
121
|
| 3 | usage or config error |
|
|
102
122
|
|
|
123
|
+
## Feedback
|
|
124
|
+
|
|
125
|
+
Something in firegoat got in your way (or your agent's)? Tell us from the terminal:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
npx firegoat feedback "eval hid claude's spend-limit error" --type bug --body "ran npx firegoat eval, got 'not graded' x3"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
103
133
|
## Develop
|
|
104
134
|
|
|
105
135
|
```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.13.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"
|
|
@@ -7072,11 +7073,19 @@ function parseConfig(text) {
|
|
|
7072
7073
|
}
|
|
7073
7074
|
if (!isRecord(raw))
|
|
7074
7075
|
fail("(root)", "must be a mapping");
|
|
7075
|
-
onlyKeys(raw, "(root)", [
|
|
7076
|
+
onlyKeys(raw, "(root)", [
|
|
7077
|
+
"runs",
|
|
7078
|
+
"setup",
|
|
7079
|
+
"workdir_setup",
|
|
7080
|
+
"vars",
|
|
7081
|
+
"env",
|
|
7082
|
+
"tests"
|
|
7083
|
+
]);
|
|
7076
7084
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7077
7085
|
const setup = commands(raw.setup, "setup");
|
|
7078
7086
|
const workdirSetup = commands(raw.workdir_setup, "workdir_setup");
|
|
7079
7087
|
const vars = parseVars(raw.vars);
|
|
7088
|
+
const env = parseEnv(raw.env, "env");
|
|
7080
7089
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
7081
7090
|
fail("tests", "must be a non-empty list");
|
|
7082
7091
|
const names = new Set;
|
|
@@ -7084,7 +7093,7 @@ function parseConfig(text) {
|
|
|
7084
7093
|
const p = `tests[${i}]`;
|
|
7085
7094
|
if (!isRecord(t))
|
|
7086
7095
|
fail(p, "must be a mapping");
|
|
7087
|
-
onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout"]);
|
|
7096
|
+
onlyKeys(t, p, ["name", "prompt", "end_goal", "checks", "timeout", "env"]);
|
|
7088
7097
|
const name = str(t.name, `${p}.name`);
|
|
7089
7098
|
if (!NAME.test(name))
|
|
7090
7099
|
fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
|
|
@@ -7107,7 +7116,8 @@ function parseConfig(text) {
|
|
|
7107
7116
|
prompt: str(t.prompt, `${p}.prompt`),
|
|
7108
7117
|
end_goal: str(t.end_goal, `${p}.end_goal`),
|
|
7109
7118
|
checks,
|
|
7110
|
-
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
|
|
7119
|
+
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
7120
|
+
env: { ...env, ...parseEnv(t.env, `${p}.env`) }
|
|
7111
7121
|
};
|
|
7112
7122
|
});
|
|
7113
7123
|
return {
|
|
@@ -7115,6 +7125,7 @@ function parseConfig(text) {
|
|
|
7115
7125
|
setup,
|
|
7116
7126
|
workdirSetup,
|
|
7117
7127
|
vars,
|
|
7128
|
+
env,
|
|
7118
7129
|
tests
|
|
7119
7130
|
};
|
|
7120
7131
|
}
|
|
@@ -7144,6 +7155,54 @@ function parseVars(raw) {
|
|
|
7144
7155
|
}
|
|
7145
7156
|
return out;
|
|
7146
7157
|
}
|
|
7158
|
+
var RESERVED_ENV = new Set([
|
|
7159
|
+
"PATH",
|
|
7160
|
+
"CLAUDE_CONFIG_DIR",
|
|
7161
|
+
"CLAUDE_SECURESTORAGE_CONFIG_DIR"
|
|
7162
|
+
]);
|
|
7163
|
+
var RUN_PLACEHOLDERS = ["run.id", "run.n", "run.workdir", "test.name"];
|
|
7164
|
+
var EVAL_PLACEHOLDERS = ["run.id"];
|
|
7165
|
+
var ANY_PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g;
|
|
7166
|
+
function parseEnv(raw, path) {
|
|
7167
|
+
if (raw === undefined)
|
|
7168
|
+
return {};
|
|
7169
|
+
if (!isRecord(raw))
|
|
7170
|
+
fail(path, "must be a mapping of NAME to value");
|
|
7171
|
+
const out = {};
|
|
7172
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
7173
|
+
const p = `${path}.${k}`;
|
|
7174
|
+
if (!VAR_KEY.test(k))
|
|
7175
|
+
fail(p, "name must be letters, digits and _, not starting with a digit");
|
|
7176
|
+
if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
|
|
7177
|
+
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
|
|
7178
|
+
if (typeof v !== "string")
|
|
7179
|
+
fail(p, "must be a string (quote numbers and booleans)");
|
|
7180
|
+
for (const m of v.matchAll(ANY_PLACEHOLDER)) {
|
|
7181
|
+
const name = m[1];
|
|
7182
|
+
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !RUN_PLACEHOLDERS.includes(name))
|
|
7183
|
+
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${RUN_PLACEHOLDERS.map((n) => `{{${n}}}`).join(", ")})`);
|
|
7184
|
+
}
|
|
7185
|
+
out[k] = v;
|
|
7186
|
+
}
|
|
7187
|
+
return out;
|
|
7188
|
+
}
|
|
7189
|
+
function isEvalLevel(value) {
|
|
7190
|
+
return [...value.matchAll(ANY_PLACEHOLDER)].every((m) => {
|
|
7191
|
+
const name = m[1];
|
|
7192
|
+
return name.startsWith("vars.") || EVAL_PLACEHOLDERS.includes(name);
|
|
7193
|
+
});
|
|
7194
|
+
}
|
|
7195
|
+
function renderEnv(env, vars, run) {
|
|
7196
|
+
const out = {};
|
|
7197
|
+
for (const [k, v] of Object.entries(env))
|
|
7198
|
+
out[k] = v.replace(ANY_PLACEHOLDER, (whole, name) => {
|
|
7199
|
+
const filled = name.startsWith("vars.") ? vars[name.slice("vars.".length)] : run[name];
|
|
7200
|
+
if (filled === undefined)
|
|
7201
|
+
throw new ConfigError(`env.${k}: no value for ${whole}`);
|
|
7202
|
+
return filled;
|
|
7203
|
+
});
|
|
7204
|
+
return out;
|
|
7205
|
+
}
|
|
7147
7206
|
var PLACEHOLDER = /\{\{\s*vars\.([A-Za-z0-9_]+)\s*\}\}/g;
|
|
7148
7207
|
function varRefs(text) {
|
|
7149
7208
|
return [...text.matchAll(PLACEHOLDER)].map((m) => m[1]);
|
|
@@ -7186,9 +7245,14 @@ var CLAUDE_FLAGS = [
|
|
|
7186
7245
|
"",
|
|
7187
7246
|
"--disable-slash-commands"
|
|
7188
7247
|
];
|
|
7189
|
-
|
|
7248
|
+
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7249
|
+
function hostEnv() {
|
|
7250
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k)));
|
|
7251
|
+
}
|
|
7252
|
+
function claudeEnv(configDir, env = {}) {
|
|
7190
7253
|
return {
|
|
7191
|
-
...
|
|
7254
|
+
...hostEnv(),
|
|
7255
|
+
...env,
|
|
7192
7256
|
CLAUDE_CONFIG_DIR: configDir,
|
|
7193
7257
|
CLAUDE_SECURESTORAGE_CONFIG_DIR: ""
|
|
7194
7258
|
};
|
|
@@ -7375,6 +7439,7 @@ script checks.
|
|
|
7375
7439
|
|
|
7376
7440
|
npx firegoat init create firegoat.yaml and .firegoat/, install the skills
|
|
7377
7441
|
npx firegoat eval run the tests (claude must be logged in: run \`claude\` once)
|
|
7442
|
+
npx firegoat feedback "<what got in the way>" tell the firegoat team
|
|
7378
7443
|
|
|
7379
7444
|
Typical flow for a coding agent driving firegoat:
|
|
7380
7445
|
1. read npx firegoat init --help, then edit firegoat.yaml
|
|
@@ -7404,6 +7469,12 @@ firegoat.yaml reference:
|
|
|
7404
7469
|
workdir_setup: <sh> | [...] in each fresh folder before the agent starts
|
|
7405
7470
|
vars: {KEY: value} defaults for {{vars.KEY}} in prompts and checks;
|
|
7406
7471
|
--var KEY=value overrides
|
|
7472
|
+
env: {NAME: value} env vars for the agent, and for workdir_setup and
|
|
7473
|
+
checks of the same run; values are strings and can
|
|
7474
|
+
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
7475
|
+
{{run.workdir}}, {{test.name}}. setup gets only the
|
|
7476
|
+
entries that use nothing but {{vars.*}}/{{run.id}}.
|
|
7477
|
+
PATH, CLAUDE_CONFIG_DIR and FIREGOAT_* are reserved.
|
|
7407
7478
|
tests:
|
|
7408
7479
|
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
7409
7480
|
prompt: ... what a user would ask the agent; one concrete outcome
|
|
@@ -7412,6 +7483,7 @@ firegoat.yaml reference:
|
|
|
7412
7483
|
checks: optional shell commands, run in the agent's folder
|
|
7413
7484
|
- run: ... afterwards; exit 0 = pass; they set eval's exit code
|
|
7414
7485
|
timeout: 600 optional, seconds
|
|
7486
|
+
env: {NAME: value} optional, on top of the top-level env:
|
|
7415
7487
|
|
|
7416
7488
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7417
7489
|
.firegoat/fixtures/ and copy them in setup or workdir_setup. setup, workdir_setup and checks get the vars as env vars,
|
|
@@ -7420,6 +7492,11 @@ firegoat.yaml reference:
|
|
|
7420
7492
|
each output), and
|
|
7421
7493
|
$FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
7422
7494
|
|
|
7495
|
+
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
7496
|
+
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
7497
|
+
npm_command, INIT_CWD; e.g. npm_config_yes=true from npx -y) are removed from
|
|
7498
|
+
every env firegoat starts, unless env: sets them.
|
|
7499
|
+
|
|
7423
7500
|
Example:
|
|
7424
7501
|
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7425
7502
|
workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
@@ -7437,8 +7514,8 @@ Example:
|
|
|
7437
7514
|
! grep -Eq '$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7438
7515
|
|
|
7439
7516
|
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7440
|
-
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value]..
|
|
7441
|
-
[--claude-bin PATH]
|
|
7517
|
+
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--parallel N] [--var KEY=value]..
|
|
7518
|
+
[--json] [--claude-bin PATH]
|
|
7442
7519
|
|
|
7443
7520
|
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
7444
7521
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with setup:'s
|
|
@@ -7451,6 +7528,7 @@ sandbox: the agent can reach the rest of your machine, including the repo.
|
|
|
7451
7528
|
|
|
7452
7529
|
test.. only these tests (by name)
|
|
7453
7530
|
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7531
|
+
--parallel N runs at once (default 4; 1 runs them one after another)
|
|
7454
7532
|
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7455
7533
|
--json the summary as JSON on stdout (or {error, exitCode} if eval
|
|
7456
7534
|
stops early); progress goes to stderr
|
|
@@ -7478,6 +7556,25 @@ Examples:
|
|
|
7478
7556
|
A run can take minutes (default timeout 600s per test).
|
|
7479
7557
|
|
|
7480
7558
|
${EXIT_CODES}`;
|
|
7559
|
+
var FEEDBACK_HELP = `Usage: firegoat feedback "<title>" [--body TEXT|-] [--type bug|feature_request|feedback]
|
|
7560
|
+
|
|
7561
|
+
Sends feedback about firegoat to the firegoat team (firegoat.ai). Coding agents: if
|
|
7562
|
+
firegoat confused you, failed you or cost you tokens, say so here: one concrete
|
|
7563
|
+
title, then what you ran and what happened in --body (- reads stdin).
|
|
7564
|
+
|
|
7565
|
+
"<title>" 8-200 characters, e.g. "eval hid claude's spend-limit error"
|
|
7566
|
+
--body TEXT details: commands, output, what you expected
|
|
7567
|
+
--type bug, feature_request or feedback (default)
|
|
7568
|
+
|
|
7569
|
+
Sent along: the firegoat version, your OS and node version, and which agent you are
|
|
7570
|
+
(claude-code/codex/cursor, from the environment). Don't include secrets: a report that
|
|
7571
|
+
looks like it contains one is refused.
|
|
7572
|
+
|
|
7573
|
+
Example:
|
|
7574
|
+
npx firegoat feedback "checks can't grep commands with quotes" --type bug \\
|
|
7575
|
+
--body "grep '"command":"...' stops at the first escaped quote"
|
|
7576
|
+
|
|
7577
|
+
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
7481
7578
|
|
|
7482
7579
|
// src/setup.ts
|
|
7483
7580
|
import { createWriteStream } from "node:fs";
|
|
@@ -7611,6 +7708,17 @@ var C = {
|
|
|
7611
7708
|
bold: "1",
|
|
7612
7709
|
cyan: "36"
|
|
7613
7710
|
};
|
|
7711
|
+
var BUTTERFLY = [" \\ /", " (_\\/_)", " (_/\\_)"];
|
|
7712
|
+
var LOGO_COLS = 10;
|
|
7713
|
+
function logoHeader(paint, title, right, width) {
|
|
7714
|
+
return BUTTERFLY.map((wing, i) => {
|
|
7715
|
+
const art = paint(C.ember, wing);
|
|
7716
|
+
if (i !== 1)
|
|
7717
|
+
return art;
|
|
7718
|
+
const used = LOGO_COLS + title.length + right.replace(ANSI, "").length;
|
|
7719
|
+
return `${art}${" ".repeat(LOGO_COLS - wing.length)}${paint(C.bold, title)}${" ".repeat(Math.max(2, width - used - 1))}${right}`;
|
|
7720
|
+
});
|
|
7721
|
+
}
|
|
7614
7722
|
function tier(rows) {
|
|
7615
7723
|
const graded = rows.filter((r) => r.state === "pass" || r.state === "fail");
|
|
7616
7724
|
if (graded.length === 0)
|
|
@@ -7645,9 +7753,10 @@ function checksPassed(r) {
|
|
|
7645
7753
|
function total(rows) {
|
|
7646
7754
|
const cost = rows.reduce((s, r) => s + (r.cost ?? 0), 0);
|
|
7647
7755
|
const turns = rows.reduce((s, r) => s + r.turns, 0);
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7756
|
+
return { cost, turns };
|
|
7757
|
+
}
|
|
7758
|
+
function duration(seconds) {
|
|
7759
|
+
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
7651
7760
|
}
|
|
7652
7761
|
function counts(rows) {
|
|
7653
7762
|
return {
|
|
@@ -7734,9 +7843,8 @@ function ttyUI(out, opts) {
|
|
|
7734
7843
|
const c = counts(rows);
|
|
7735
7844
|
const doneCount = c.pass + c.fail + c.error;
|
|
7736
7845
|
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
7737
|
-
const title = ` \uD83E\uDD8B firegoat eval · ${opts.project}`;
|
|
7738
7846
|
const out = [
|
|
7739
|
-
|
|
7847
|
+
...logoHeader(p, `firegoat eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
|
|
7740
7848
|
rule
|
|
7741
7849
|
];
|
|
7742
7850
|
if (authText)
|
|
@@ -7772,22 +7880,23 @@ function ttyUI(out, opts) {
|
|
|
7772
7880
|
if (!finished) {
|
|
7773
7881
|
const track = Math.max(10, w - 34);
|
|
7774
7882
|
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
7775
|
-
const
|
|
7776
|
-
out.push(` ${p(C.ember,
|
|
7883
|
+
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
7884
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · $${t.cost.toFixed(2)} so far`)}`);
|
|
7777
7885
|
return out;
|
|
7778
7886
|
}
|
|
7779
7887
|
const tr = tier(rows);
|
|
7780
7888
|
const goated = c.pass === rows.length && rows.length > 0;
|
|
7781
7889
|
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
7782
|
-
const
|
|
7783
|
-
|
|
7784
|
-
out.push(p(C.
|
|
7890
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
7891
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `tier ${tr}` : "";
|
|
7892
|
+
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
7893
|
+
out.push(p(C.gray, ` total $${t.cost.toFixed(2)} · ${t.turns} turns · ${duration(finished.seconds)}`));
|
|
7785
7894
|
out.push(p(C.gray, ` runs ${relative2(process.cwd(), finished.runsDir)}/`));
|
|
7786
7895
|
out.push(` checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
7787
7896
|
return out;
|
|
7788
7897
|
};
|
|
7789
7898
|
const rowsOf = (line) => {
|
|
7790
|
-
const visible = line.replace(ANSI, "")
|
|
7899
|
+
const visible = line.replace(ANSI, "");
|
|
7791
7900
|
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
7792
7901
|
};
|
|
7793
7902
|
const render = () => {
|
|
@@ -7817,9 +7926,9 @@ function ttyUI(out, opts) {
|
|
|
7817
7926
|
render();
|
|
7818
7927
|
},
|
|
7819
7928
|
update: render,
|
|
7820
|
-
done: ({ runId, runsDir }) => {
|
|
7929
|
+
done: ({ runId, runsDir, seconds }) => {
|
|
7821
7930
|
clearInterval(timer);
|
|
7822
|
-
finished = { runId, runsDir };
|
|
7931
|
+
finished = { runId, runsDir, seconds };
|
|
7823
7932
|
render();
|
|
7824
7933
|
}
|
|
7825
7934
|
};
|
|
@@ -7854,6 +7963,7 @@ class UsageError extends Error {
|
|
|
7854
7963
|
function parseArgs(args) {
|
|
7855
7964
|
const opts = {
|
|
7856
7965
|
names: [],
|
|
7966
|
+
parallel: 4,
|
|
7857
7967
|
json: false,
|
|
7858
7968
|
vars: {},
|
|
7859
7969
|
claudeBin: process.env.FIREGOAT_CLAUDE_BIN || "claude"
|
|
@@ -7886,6 +7996,12 @@ function parseArgs(args) {
|
|
|
7886
7996
|
if (!/^\d+$/.test(v) || n < 1)
|
|
7887
7997
|
throw new UsageError(`--runs must be a positive integer, got ${v}`);
|
|
7888
7998
|
opts.runs = n;
|
|
7999
|
+
} else if (flag === "--parallel") {
|
|
8000
|
+
const v = value();
|
|
8001
|
+
const n = Number(v);
|
|
8002
|
+
if (!/^\d+$/.test(v) || n < 1)
|
|
8003
|
+
throw new UsageError(`--parallel must be a positive integer, got ${v}`);
|
|
8004
|
+
opts.parallel = n;
|
|
7889
8005
|
} else if (flag === "--claude-bin")
|
|
7890
8006
|
opts.claudeBin = value();
|
|
7891
8007
|
else if (a.startsWith("-"))
|
|
@@ -7942,7 +8058,13 @@ async function runOne(opts) {
|
|
|
7942
8058
|
`);
|
|
7943
8059
|
return r;
|
|
7944
8060
|
};
|
|
7945
|
-
const
|
|
8061
|
+
const env = renderEnv(test.env, opts.vars, {
|
|
8062
|
+
"run.id": opts.runId,
|
|
8063
|
+
"run.n": String(n),
|
|
8064
|
+
"run.workdir": workdir,
|
|
8065
|
+
"test.name": test.name
|
|
8066
|
+
});
|
|
8067
|
+
const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
|
|
7946
8068
|
if (opts.workdirSetup.length > 0) {
|
|
7947
8069
|
const error = await runSteps({
|
|
7948
8070
|
what: "workdir_setup",
|
|
@@ -7959,7 +8081,7 @@ async function runOne(opts) {
|
|
|
7959
8081
|
bin: opts.claudeBin,
|
|
7960
8082
|
prompt: r.prompt,
|
|
7961
8083
|
cwd: workdir,
|
|
7962
|
-
env: { ...claudeEnv(opts.configDir), PATH: opts.stepEnv.PATH },
|
|
8084
|
+
env: { ...claudeEnv(opts.configDir, env), PATH: opts.stepEnv.PATH },
|
|
7963
8085
|
stderr,
|
|
7964
8086
|
timeoutMs: test.timeout * 1000,
|
|
7965
8087
|
onEvent: opts.onEvent
|
|
@@ -8044,7 +8166,11 @@ async function evalCommand(args, root) {
|
|
|
8044
8166
|
const runs = opts.runs ?? config.runs;
|
|
8045
8167
|
const vars = { ...config.vars, ...opts.vars };
|
|
8046
8168
|
const missing = [
|
|
8047
|
-
...new Set(tests.flatMap((t) => [
|
|
8169
|
+
...new Set(tests.flatMap((t) => [
|
|
8170
|
+
t.prompt,
|
|
8171
|
+
...t.checks.map((c) => c.run),
|
|
8172
|
+
...Object.values(t.env)
|
|
8173
|
+
]).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
8048
8174
|
];
|
|
8049
8175
|
if (missing.length > 0)
|
|
8050
8176
|
return fatal(EXIT.usage, `no value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`);
|
|
@@ -8062,12 +8188,13 @@ async function evalCommand(args, root) {
|
|
|
8062
8188
|
});
|
|
8063
8189
|
if (loginProblem)
|
|
8064
8190
|
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
8191
|
+
const evalStarted = Date.now();
|
|
8065
8192
|
const runId = newRunId();
|
|
8066
8193
|
const runDir = join4(firegoatDir, "runs", runId);
|
|
8067
8194
|
const out = join4(runDir, "out");
|
|
8068
8195
|
mkdirSync2(out, { recursive: true });
|
|
8069
8196
|
const stepEnv = {
|
|
8070
|
-
...
|
|
8197
|
+
...hostEnv(),
|
|
8071
8198
|
...vars,
|
|
8072
8199
|
FIREGOAT_OUT: out,
|
|
8073
8200
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
@@ -8085,7 +8212,10 @@ async function evalCommand(args, root) {
|
|
|
8085
8212
|
what: "setup",
|
|
8086
8213
|
commands: config.setup,
|
|
8087
8214
|
cwd: root,
|
|
8088
|
-
env:
|
|
8215
|
+
env: {
|
|
8216
|
+
...stepEnv,
|
|
8217
|
+
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId })
|
|
8218
|
+
},
|
|
8089
8219
|
logPath: join4(runDir, "setup.txt")
|
|
8090
8220
|
});
|
|
8091
8221
|
const secs = Math.round((Date.now() - setupStarted) / 1000);
|
|
@@ -8094,60 +8224,247 @@ async function evalCommand(args, root) {
|
|
|
8094
8224
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
8095
8225
|
}
|
|
8096
8226
|
ui.runs(rows);
|
|
8097
|
-
const results =
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8227
|
+
const results = new Array(rows.length);
|
|
8228
|
+
const runAt = async (i) => {
|
|
8229
|
+
const row = rows[i];
|
|
8230
|
+
const test = tests.find((t) => t.name === row.test);
|
|
8231
|
+
row.state = "running";
|
|
8232
|
+
ui.update();
|
|
8233
|
+
const r = await runOne({
|
|
8234
|
+
onEvent: (e) => {
|
|
8235
|
+
if (e.type !== "assistant")
|
|
8236
|
+
return;
|
|
8237
|
+
row.turns++;
|
|
8238
|
+
const content = e.message?.content;
|
|
8239
|
+
if (!Array.isArray(content))
|
|
8240
|
+
return;
|
|
8241
|
+
for (const b of content) {
|
|
8242
|
+
const cmd = b.input?.command;
|
|
8243
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
8244
|
+
row.command = cmd.split(`
|
|
8115
8245
|
`)[0];
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8246
|
+
}
|
|
8247
|
+
},
|
|
8248
|
+
test,
|
|
8249
|
+
n: row.n,
|
|
8250
|
+
runId,
|
|
8251
|
+
runDir,
|
|
8252
|
+
claudeBin: claudePath,
|
|
8253
|
+
configDir,
|
|
8254
|
+
workdirSetup: config.workdirSetup,
|
|
8255
|
+
stepEnv,
|
|
8256
|
+
vars
|
|
8257
|
+
});
|
|
8258
|
+
results[i] = r;
|
|
8259
|
+
Object.assign(row, {
|
|
8260
|
+
state: r.status,
|
|
8261
|
+
turns: r.num_turns ?? row.turns,
|
|
8262
|
+
reason: r.reason,
|
|
8263
|
+
checks: r.checks,
|
|
8264
|
+
cost: r.cost,
|
|
8265
|
+
seconds: Math.round(r.duration_ms / 1000),
|
|
8266
|
+
command: undefined
|
|
8267
|
+
});
|
|
8268
|
+
ui.update();
|
|
8269
|
+
};
|
|
8270
|
+
let next = 0;
|
|
8271
|
+
const worker = async () => {
|
|
8272
|
+
while (next < rows.length)
|
|
8273
|
+
await runAt(next++);
|
|
8274
|
+
};
|
|
8275
|
+
await Promise.all(Array.from({ length: Math.min(opts.parallel, rows.length) }, worker));
|
|
8139
8276
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
8140
8277
|
for (const r of results)
|
|
8141
8278
|
totals[r.status]++;
|
|
8142
8279
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
8143
8280
|
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
8144
8281
|
`);
|
|
8145
|
-
ui.done({
|
|
8282
|
+
ui.done({
|
|
8283
|
+
runId,
|
|
8284
|
+
runsDir: runDir,
|
|
8285
|
+
rows,
|
|
8286
|
+
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
8287
|
+
});
|
|
8146
8288
|
if (opts.json)
|
|
8147
8289
|
console.log(JSON.stringify(doc, null, 2));
|
|
8148
8290
|
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
8149
8291
|
}
|
|
8150
8292
|
|
|
8293
|
+
// src/feedback.ts
|
|
8294
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8295
|
+
|
|
8296
|
+
// node_modules/@firegoat/feedback/dist/index.js
|
|
8297
|
+
var VERSION = "0.0.0";
|
|
8298
|
+
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
8299
|
+
var TIMEOUT_MS = 15000;
|
|
8300
|
+
function envBaseUrl() {
|
|
8301
|
+
return globalThis.process?.env?.FEEDBACK_API_URL;
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8304
|
+
class FeedbackError extends Error {
|
|
8305
|
+
status;
|
|
8306
|
+
code;
|
|
8307
|
+
retryAfterSeconds;
|
|
8308
|
+
detected;
|
|
8309
|
+
constructor(status, code, message, retryAfterSeconds, detected) {
|
|
8310
|
+
super(message);
|
|
8311
|
+
this.status = status;
|
|
8312
|
+
this.code = code;
|
|
8313
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
8314
|
+
this.detected = detected;
|
|
8315
|
+
this.name = "FeedbackError";
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
var TITLE_MIN = 8;
|
|
8319
|
+
var TITLE_MAX = 200;
|
|
8320
|
+
|
|
8321
|
+
class FeedbackClient {
|
|
8322
|
+
domain;
|
|
8323
|
+
baseUrl;
|
|
8324
|
+
token;
|
|
8325
|
+
fetchImpl;
|
|
8326
|
+
constructor(options) {
|
|
8327
|
+
if (!options.domain?.trim()) {
|
|
8328
|
+
throw new Error("FeedbackClient: `domain` is required (your product's domain, e.g. example.com)");
|
|
8329
|
+
}
|
|
8330
|
+
this.domain = options.domain.trim();
|
|
8331
|
+
this.baseUrl = (options.baseUrl ?? envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
8332
|
+
this.token = options.token;
|
|
8333
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
8334
|
+
}
|
|
8335
|
+
async registerDevice() {
|
|
8336
|
+
const data = await this.post("/api/feedback/device", undefined);
|
|
8337
|
+
if (typeof data.token !== "string" || !data.token) {
|
|
8338
|
+
throw new FeedbackError(201, "invalid_response", "server response missing token");
|
|
8339
|
+
}
|
|
8340
|
+
this.token = data.token;
|
|
8341
|
+
return data.token;
|
|
8342
|
+
}
|
|
8343
|
+
async send(input) {
|
|
8344
|
+
const title = input.title.trim();
|
|
8345
|
+
if (title.length < TITLE_MIN || title.length > TITLE_MAX) {
|
|
8346
|
+
throw new FeedbackError(400, "invalid_request", `title must be ${TITLE_MIN}-${TITLE_MAX} characters, got ${title.length}`);
|
|
8347
|
+
}
|
|
8348
|
+
const meta = {
|
|
8349
|
+
...input.meta,
|
|
8350
|
+
cliVersion: `@firegoat/feedback@${VERSION}`,
|
|
8351
|
+
feedbackType: input.type
|
|
8352
|
+
};
|
|
8353
|
+
const data = await this.post("/api/feedback", {
|
|
8354
|
+
domain: this.domain,
|
|
8355
|
+
title,
|
|
8356
|
+
body: input.body ?? "",
|
|
8357
|
+
meta,
|
|
8358
|
+
maskSecrets: input.maskSecrets ?? false
|
|
8359
|
+
});
|
|
8360
|
+
if (typeof data.id !== "string" || !data.id) {
|
|
8361
|
+
throw new FeedbackError(201, "invalid_response", "server response missing id");
|
|
8362
|
+
}
|
|
8363
|
+
return { id: data.id, similarCount: data.similarCount ?? 0 };
|
|
8364
|
+
}
|
|
8365
|
+
async post(path, body) {
|
|
8366
|
+
const headers = {
|
|
8367
|
+
"content-type": "application/json"
|
|
8368
|
+
};
|
|
8369
|
+
if (this.token)
|
|
8370
|
+
headers["x-feedback-token"] = this.token;
|
|
8371
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
8372
|
+
method: "POST",
|
|
8373
|
+
headers,
|
|
8374
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
8375
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8376
|
+
});
|
|
8377
|
+
const text = await res.text();
|
|
8378
|
+
let data;
|
|
8379
|
+
try {
|
|
8380
|
+
data = JSON.parse(text);
|
|
8381
|
+
} catch {
|
|
8382
|
+
throw new FeedbackError(res.status, "invalid_response", `non-JSON response (${res.status}): ${text.slice(0, 200)}`);
|
|
8383
|
+
}
|
|
8384
|
+
if (!res.ok) {
|
|
8385
|
+
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);
|
|
8386
|
+
}
|
|
8387
|
+
return data;
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8390
|
+
|
|
8391
|
+
// src/feedback.ts
|
|
8392
|
+
var TYPES = ["bug", "feature_request", "feedback"];
|
|
8393
|
+
function agentName() {
|
|
8394
|
+
if (process.env.CLAUDECODE)
|
|
8395
|
+
return "claude-code";
|
|
8396
|
+
if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX)
|
|
8397
|
+
return "codex";
|
|
8398
|
+
if (process.env.CURSOR_TRACE_ID)
|
|
8399
|
+
return "cursor";
|
|
8400
|
+
return;
|
|
8401
|
+
}
|
|
8402
|
+
async function feedbackCommand(args) {
|
|
8403
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
8404
|
+
console.log(FEEDBACK_HELP);
|
|
8405
|
+
return args.length === 0 ? EXIT.usage : EXIT.pass;
|
|
8406
|
+
}
|
|
8407
|
+
const titleParts = [];
|
|
8408
|
+
let body;
|
|
8409
|
+
let type = "feedback";
|
|
8410
|
+
for (let i = 0;i < args.length; i++) {
|
|
8411
|
+
const a = args[i];
|
|
8412
|
+
const value = () => {
|
|
8413
|
+
const v = args[++i];
|
|
8414
|
+
if (v === undefined)
|
|
8415
|
+
throw new Error(`${a} needs a value`);
|
|
8416
|
+
return v;
|
|
8417
|
+
};
|
|
8418
|
+
try {
|
|
8419
|
+
if (a === "--body") {
|
|
8420
|
+
const v = value();
|
|
8421
|
+
body = v === "-" ? readFileSync2(0, "utf8") : v;
|
|
8422
|
+
} else if (a === "--type") {
|
|
8423
|
+
const v = value();
|
|
8424
|
+
if (!TYPES.includes(v))
|
|
8425
|
+
throw new Error(`--type must be one of ${TYPES.join(", ")}, got ${v}`);
|
|
8426
|
+
type = v;
|
|
8427
|
+
} else if (a.startsWith("-"))
|
|
8428
|
+
throw new Error(`unknown flag: ${a}`);
|
|
8429
|
+
else
|
|
8430
|
+
titleParts.push(a);
|
|
8431
|
+
} catch (e) {
|
|
8432
|
+
console.error(`firegoat feedback: ${e.message}`);
|
|
8433
|
+
return EXIT.usage;
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
const title = titleParts.join(" ").trim();
|
|
8437
|
+
if (title.length < 8 || title.length > 200) {
|
|
8438
|
+
console.error(`firegoat feedback: the title must be 8-200 characters (got ${title.length})`);
|
|
8439
|
+
return EXIT.usage;
|
|
8440
|
+
}
|
|
8441
|
+
const client = new FeedbackClient({ domain: "firegoat.ai" });
|
|
8442
|
+
try {
|
|
8443
|
+
const agent = agentName();
|
|
8444
|
+
const { id, similarCount } = await client.send({
|
|
8445
|
+
title,
|
|
8446
|
+
body: body?.trim() ? `${body.trim()}
|
|
8447
|
+
|
|
8448
|
+
firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
8449
|
+
type,
|
|
8450
|
+
meta: {
|
|
8451
|
+
...agent ? { agent } : {},
|
|
8452
|
+
os: process.platform,
|
|
8453
|
+
nodeVersion: process.version
|
|
8454
|
+
}
|
|
8455
|
+
});
|
|
8456
|
+
console.log(`sent ${type} ${id} to the firegoat team${similarCount > 0 ? ` (${similarCount} similar report${similarCount === 1 ? "" : "s"} already)` : ""}. thanks.`);
|
|
8457
|
+
return EXIT.pass;
|
|
8458
|
+
} catch (e) {
|
|
8459
|
+
if (e instanceof FeedbackError) {
|
|
8460
|
+
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.` : "";
|
|
8461
|
+
console.error(`firegoat feedback: ${e.code}: ${e.message}.${hint}`);
|
|
8462
|
+
return e.code === "invalid_request" || e.code === "possible_secret" ? EXIT.usage : EXIT.notGraded;
|
|
8463
|
+
}
|
|
8464
|
+
throw e;
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
|
|
8151
8468
|
// src/init.ts
|
|
8152
8469
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8153
8470
|
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -8195,6 +8512,12 @@ runs: 1
|
|
|
8195
8512
|
# setup runs in this folder, so: setup: cp -R .firegoat/fixtures/. "$FIREGOAT_OUT"
|
|
8196
8513
|
# then e.g. workdir_setup: cp "$FIREGOAT_OUT/wizard.mjs" .
|
|
8197
8514
|
|
|
8515
|
+
# Optional: env vars for the agent (and for workdir_setup and checks of the same
|
|
8516
|
+
# run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
|
|
8517
|
+
# {{run.id}}, {{run.n}}, {{run.workdir}}, {{test.name}}. A test can add its own env:.
|
|
8518
|
+
# env:
|
|
8519
|
+
# MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{run.n}}"
|
|
8520
|
+
|
|
8198
8521
|
tests:
|
|
8199
8522
|
- name: quickstart
|
|
8200
8523
|
# What a user would ask their agent. Edit this.
|
|
@@ -8261,9 +8584,8 @@ ${INIT_HELP}`);
|
|
|
8261
8584
|
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
8262
8585
|
const step = (n, label, status) => console.log(` ${paint("32", "●")} ${paint("90", String(n).padStart(2, "0"))} ${label.padEnd(28)}${paint("90", status)}`);
|
|
8263
8586
|
if (interactive) {
|
|
8264
|
-
const
|
|
8265
|
-
|
|
8266
|
-
console.log(`${paint("38;5;208", " \uD83E\uDD8B")} firegoat init${" ".repeat(Math.max(2, width - title.length - tag.length - 2))}${paint("90", tag)}`);
|
|
8587
|
+
for (const line of logoHeader(paint, "firegoat init", paint("90", "Get agent-ready."), width))
|
|
8588
|
+
console.log(line);
|
|
8267
8589
|
console.log(rule);
|
|
8268
8590
|
}
|
|
8269
8591
|
ensureFiregoatDir(cwd);
|
|
@@ -8341,6 +8663,8 @@ async function main(argv) {
|
|
|
8341
8663
|
return initCommand(rest, process.cwd());
|
|
8342
8664
|
case "eval":
|
|
8343
8665
|
return evalCommand(rest, process.cwd());
|
|
8666
|
+
case "feedback":
|
|
8667
|
+
return feedbackCommand(rest);
|
|
8344
8668
|
default:
|
|
8345
8669
|
console.error(`unknown command: ${cmd}
|
|
8346
8670
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firegoat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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"
|