firegoat 0.9.3 → 0.10.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 +4 -4
- package/dist/cli.js +157 -40
- 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.
|
|
5
|
+
Status: early (v0.10). See `plan.md`.
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ tests:
|
|
|
30
30
|
and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
|
|
31
31
|
end_goal: A script sends one event with the SDK's client and prints its id.
|
|
32
32
|
checks:
|
|
33
|
-
- run: grep -qE 'evt_[A-Za-z0-9]+' "$
|
|
33
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`setup` builds your package once; `workdir_setup` installs that build into each fresh folder, so the agent uses your local code, not the registry.
|
|
@@ -44,7 +44,7 @@ tests:
|
|
|
44
44
|
prompt: Deploy the hello-world template to a preview environment with the acme CLI and print the URL.
|
|
45
45
|
end_goal: The agent finds the right acme commands and ends with a working preview URL.
|
|
46
46
|
checks:
|
|
47
|
-
- run: grep -q '
|
|
47
|
+
- run: grep -q '\$ acme deploy' "$FIREGOAT_TRANSCRIPT"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Whatever `setup` writes to `$FIREGOAT_OUT` is first on the agent's `PATH`. That only helps for binaries called by bare name: if users run your CLI via `npx`/`uvx`, pack and install it like the npm example, or `npx` fetches the registry release.
|
|
@@ -87,7 +87,7 @@ npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
|
87
87
|
- 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
88
|
- 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`.
|
|
89
89
|
- 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
|
-
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `run.json` (status, prompt, end_goal, check results, paths), `work/`, logs. `summary.json` covers the whole eval; `--json` prints it.
|
|
90
|
+
- 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
91
|
|
|
92
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`.
|
|
93
93
|
|
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.10.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7169,10 +7169,10 @@ import {
|
|
|
7169
7169
|
createWriteStream as createWriteStream2,
|
|
7170
7170
|
existsSync as existsSync2,
|
|
7171
7171
|
mkdirSync as mkdirSync2,
|
|
7172
|
-
readdirSync,
|
|
7173
|
-
writeFileSync as
|
|
7172
|
+
readdirSync as readdirSync2,
|
|
7173
|
+
writeFileSync as writeFileSync3
|
|
7174
7174
|
} from "node:fs";
|
|
7175
|
-
import { delimiter as delimiter2, join as
|
|
7175
|
+
import { 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";
|
|
@@ -7239,6 +7239,7 @@ async function runClaude(opts) {
|
|
|
7239
7239
|
});
|
|
7240
7240
|
if (exited.error)
|
|
7241
7241
|
return {
|
|
7242
|
+
events: [],
|
|
7242
7243
|
ok: false,
|
|
7243
7244
|
reason: `could not start ${opts.bin}: ${exited.error.message}`
|
|
7244
7245
|
};
|
|
@@ -7250,16 +7251,21 @@ async function runClaude(opts) {
|
|
|
7250
7251
|
return;
|
|
7251
7252
|
}
|
|
7252
7253
|
});
|
|
7253
|
-
const init = events
|
|
7254
|
-
if (init
|
|
7254
|
+
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7255
|
+
if (!init) {
|
|
7256
|
+
const said = stdout.trim().split(`
|
|
7257
|
+
`)[0]?.slice(0, 300);
|
|
7255
7258
|
return {
|
|
7259
|
+
events,
|
|
7256
7260
|
ok: false,
|
|
7257
|
-
reason: `claude did not start a session (exit ${exited.code ?? "by signal"}), see stderr.txt`
|
|
7261
|
+
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7258
7262
|
};
|
|
7263
|
+
}
|
|
7259
7264
|
const sessionId = String(init.session_id);
|
|
7260
7265
|
const result = events.findLast((e) => e?.type === "result");
|
|
7261
7266
|
if (exited.timedOut)
|
|
7262
7267
|
return {
|
|
7268
|
+
events,
|
|
7263
7269
|
ok: false,
|
|
7264
7270
|
reason: `timed out after ${opts.timeoutMs / 1000}s`,
|
|
7265
7271
|
result,
|
|
@@ -7267,18 +7273,20 @@ async function runClaude(opts) {
|
|
|
7267
7273
|
};
|
|
7268
7274
|
if (!result)
|
|
7269
7275
|
return {
|
|
7276
|
+
events,
|
|
7270
7277
|
ok: false,
|
|
7271
7278
|
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7272
7279
|
sessionId
|
|
7273
7280
|
};
|
|
7274
7281
|
if (result.is_error === true || result.subtype !== "success")
|
|
7275
7282
|
return {
|
|
7283
|
+
events,
|
|
7276
7284
|
ok: false,
|
|
7277
|
-
reason: `claude result is an error (subtype=${result.subtype})`,
|
|
7285
|
+
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7278
7286
|
result,
|
|
7279
7287
|
sessionId
|
|
7280
7288
|
};
|
|
7281
|
-
return { ok: true, result, sessionId };
|
|
7289
|
+
return { events, ok: true, result, sessionId };
|
|
7282
7290
|
}
|
|
7283
7291
|
async function runShell(opts) {
|
|
7284
7292
|
opts.log.write(`$ ${opts.run}
|
|
@@ -7394,8 +7402,10 @@ firegoat.yaml reference:
|
|
|
7394
7402
|
|
|
7395
7403
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7396
7404
|
.firegoat/fixtures/ and copy them in setup or workdir_setup. setup, workdir_setup and checks get the vars as env vars,
|
|
7397
|
-
plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR
|
|
7398
|
-
|
|
7405
|
+
plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR. Checks also get $FIREGOAT_TRANSCRIPT, a short
|
|
7406
|
+
plain-text transcript ("[1] $ <command>" per shell call, "[2] Write <path>", then
|
|
7407
|
+
each output), and
|
|
7408
|
+
$FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
7399
7409
|
|
|
7400
7410
|
Example:
|
|
7401
7411
|
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -7409,9 +7419,9 @@ Example:
|
|
|
7409
7419
|
A script sends one event with the SDK's client (not raw HTTP) and prints its
|
|
7410
7420
|
id, without installing acme from the registry.
|
|
7411
7421
|
checks:
|
|
7412
|
-
- run: grep -qE 'evt_[A-Za-z0-9]+' "$
|
|
7422
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
7413
7423
|
- run: |
|
|
7414
|
-
! grep -Eq 'npm (install|i)
|
|
7424
|
+
! grep -Eq '$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7415
7425
|
|
|
7416
7426
|
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7417
7427
|
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json]
|
|
@@ -7437,12 +7447,15 @@ Output: .firegoat/runs/<runId>/
|
|
|
7437
7447
|
summary.json every run, plus totals
|
|
7438
7448
|
<test>/<n>/run.json one run: status and reason (from the checks), prompt,
|
|
7439
7449
|
end_goal, checks [{run, code}], and paths: workdir,
|
|
7440
|
-
session (claude's session file), stderr,
|
|
7450
|
+
transcript, session (claude's session file), stderr,
|
|
7451
|
+
checksLog
|
|
7452
|
+
<test>/<n>/transcript.txt what the agent did: each tool call with its command,
|
|
7453
|
+
short output, its final answer, files it made
|
|
7441
7454
|
setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
|
|
7442
7455
|
|
|
7443
7456
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
7444
|
-
yours to judge
|
|
7445
|
-
|
|
7457
|
+
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
7458
|
+
(often 50x larger).
|
|
7446
7459
|
|
|
7447
7460
|
Examples:
|
|
7448
7461
|
npx firegoat eval --json all tests, JSON summary on stdout
|
|
@@ -7476,14 +7489,106 @@ async function runSteps(opts) {
|
|
|
7476
7489
|
}
|
|
7477
7490
|
}
|
|
7478
7491
|
|
|
7492
|
+
// src/transcript.ts
|
|
7493
|
+
import { readdirSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7494
|
+
import { join as join2, relative } from "node:path";
|
|
7495
|
+
var OUTPUT_LINES = 15;
|
|
7496
|
+
var OUTPUT_CHARS = 1500;
|
|
7497
|
+
var TEXT_CHARS = 1500;
|
|
7498
|
+
var SKIP_DIRS = new Set(["node_modules", ".git", ".venv", "__pycache__"]);
|
|
7499
|
+
function truncate(text, maxLines, maxChars) {
|
|
7500
|
+
let out = text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
7501
|
+
const lines = out.split(`
|
|
7502
|
+
`);
|
|
7503
|
+
if (lines.length > maxLines)
|
|
7504
|
+
out = lines.slice(0, maxLines).join(`
|
|
7505
|
+
`);
|
|
7506
|
+
return out.length < text.length ? `${out}
|
|
7507
|
+
… (${text.length - out.length} more chars)` : out;
|
|
7508
|
+
}
|
|
7509
|
+
function indent(text) {
|
|
7510
|
+
return text.split(`
|
|
7511
|
+
`).map((l) => ` ${l}`).join(`
|
|
7512
|
+
`);
|
|
7513
|
+
}
|
|
7514
|
+
function describeCall(name, input) {
|
|
7515
|
+
if (typeof input.command === "string")
|
|
7516
|
+
return `$ ${input.command}`;
|
|
7517
|
+
for (const key of ["file_path", "path", "url", "pattern", "query"])
|
|
7518
|
+
if (typeof input[key] === "string")
|
|
7519
|
+
return `${name} ${input[key]}`;
|
|
7520
|
+
return `${name} ${truncate(JSON.stringify(input), 1, 300)}`;
|
|
7521
|
+
}
|
|
7522
|
+
function resultText(content) {
|
|
7523
|
+
if (typeof content === "string")
|
|
7524
|
+
return content;
|
|
7525
|
+
if (Array.isArray(content))
|
|
7526
|
+
return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
|
|
7527
|
+
`);
|
|
7528
|
+
return "";
|
|
7529
|
+
}
|
|
7530
|
+
function workdirFiles(dir) {
|
|
7531
|
+
const out = [];
|
|
7532
|
+
const walk = (d) => {
|
|
7533
|
+
for (const name of readdirSync(d)) {
|
|
7534
|
+
if (SKIP_DIRS.has(name))
|
|
7535
|
+
continue;
|
|
7536
|
+
const p = join2(d, name);
|
|
7537
|
+
if (statSync(p).isDirectory())
|
|
7538
|
+
walk(p);
|
|
7539
|
+
else
|
|
7540
|
+
out.push(relative(dir, p));
|
|
7541
|
+
}
|
|
7542
|
+
};
|
|
7543
|
+
walk(dir);
|
|
7544
|
+
return out.sort();
|
|
7545
|
+
}
|
|
7546
|
+
function writeTranscript(opts) {
|
|
7547
|
+
const out = [`# ${opts.title}`, "", "## prompt", opts.prompt, "", "## steps"];
|
|
7548
|
+
let step = 0;
|
|
7549
|
+
let final = "";
|
|
7550
|
+
let lastText = -1;
|
|
7551
|
+
for (const e of opts.events) {
|
|
7552
|
+
const message = e?.message;
|
|
7553
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
7554
|
+
if (e?.type === "assistant")
|
|
7555
|
+
for (const b of content) {
|
|
7556
|
+
if (b.type === "tool_use")
|
|
7557
|
+
out.push(`[${++step}] ${describeCall(String(b.name), b.input ?? {})}`);
|
|
7558
|
+
else if (b.type === "text" && typeof b.text === "string") {
|
|
7559
|
+
lastText = out.length;
|
|
7560
|
+
out.push(`agent: ${truncate(b.text, 30, TEXT_CHARS)}`);
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7563
|
+
else if (e?.type === "user")
|
|
7564
|
+
for (const b of content) {
|
|
7565
|
+
if (b.type !== "tool_result")
|
|
7566
|
+
continue;
|
|
7567
|
+
const text = resultText(b.content).trimEnd();
|
|
7568
|
+
const status = b.is_error === true ? "error" : "ok";
|
|
7569
|
+
out.push(text ? ` → ${status}
|
|
7570
|
+
${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
7571
|
+
}
|
|
7572
|
+
else if (e?.type === "result" && typeof e.result === "string")
|
|
7573
|
+
final = e.result;
|
|
7574
|
+
}
|
|
7575
|
+
if (lastText === out.length - 1 && final && out[lastText] === `agent: ${truncate(final, 30, TEXT_CHARS)}`)
|
|
7576
|
+
out.pop();
|
|
7577
|
+
out.push("", "## final answer", final || "(none)");
|
|
7578
|
+
out.push("", "## files in the workdir", ...workdirFiles(opts.workdir));
|
|
7579
|
+
writeFileSync2(opts.path, `${out.join(`
|
|
7580
|
+
`)}
|
|
7581
|
+
`);
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7479
7584
|
// src/which.ts
|
|
7480
|
-
import { accessSync, constants, statSync } from "node:fs";
|
|
7481
|
-
import { delimiter, join as
|
|
7585
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
7586
|
+
import { delimiter, join as join3, resolve } from "node:path";
|
|
7482
7587
|
function resolveBin(bin) {
|
|
7483
7588
|
const executable = (p) => {
|
|
7484
7589
|
try {
|
|
7485
7590
|
accessSync(p, constants.X_OK);
|
|
7486
|
-
return
|
|
7591
|
+
return statSync2(p).isFile();
|
|
7487
7592
|
} catch {
|
|
7488
7593
|
return false;
|
|
7489
7594
|
}
|
|
@@ -7491,8 +7596,8 @@ function resolveBin(bin) {
|
|
|
7491
7596
|
if (bin.includes("/"))
|
|
7492
7597
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7493
7598
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
7494
|
-
if (dir && executable(
|
|
7495
|
-
return
|
|
7599
|
+
if (dir && executable(join3(dir, bin)))
|
|
7600
|
+
return join3(dir, bin);
|
|
7496
7601
|
}
|
|
7497
7602
|
return;
|
|
7498
7603
|
}
|
|
@@ -7547,11 +7652,11 @@ function parseArgs(args) {
|
|
|
7547
7652
|
return opts;
|
|
7548
7653
|
}
|
|
7549
7654
|
function findSession(configDir, sessionId) {
|
|
7550
|
-
const projects =
|
|
7655
|
+
const projects = join4(configDir, "projects");
|
|
7551
7656
|
if (!existsSync2(projects))
|
|
7552
7657
|
return null;
|
|
7553
|
-
for (const d of
|
|
7554
|
-
const f =
|
|
7658
|
+
for (const d of readdirSync2(projects)) {
|
|
7659
|
+
const f = join4(projects, d, `${sessionId}.jsonl`);
|
|
7555
7660
|
if (existsSync2(f))
|
|
7556
7661
|
return f;
|
|
7557
7662
|
}
|
|
@@ -7573,8 +7678,8 @@ function newRunId() {
|
|
|
7573
7678
|
async function runOne(opts) {
|
|
7574
7679
|
const { test, n } = opts;
|
|
7575
7680
|
const started = Date.now();
|
|
7576
|
-
const dir =
|
|
7577
|
-
const workdir =
|
|
7681
|
+
const dir = join4(opts.runDir, test.name, String(n));
|
|
7682
|
+
const workdir = join4(dir, "work");
|
|
7578
7683
|
mkdirSync2(workdir, { recursive: true });
|
|
7579
7684
|
const r = {
|
|
7580
7685
|
test: test.name,
|
|
@@ -7587,8 +7692,9 @@ async function runOne(opts) {
|
|
|
7587
7692
|
dir,
|
|
7588
7693
|
workdir,
|
|
7589
7694
|
session: null,
|
|
7590
|
-
|
|
7591
|
-
|
|
7695
|
+
transcript: join4(dir, "transcript.txt"),
|
|
7696
|
+
stderr: join4(dir, "stderr.txt"),
|
|
7697
|
+
checksLog: join4(dir, "checks.txt"),
|
|
7592
7698
|
num_turns: null,
|
|
7593
7699
|
cost: null,
|
|
7594
7700
|
duration_ms: 0
|
|
@@ -7597,7 +7703,7 @@ async function runOne(opts) {
|
|
|
7597
7703
|
r.status = status;
|
|
7598
7704
|
r.reason = reason;
|
|
7599
7705
|
r.duration_ms = Date.now() - started;
|
|
7600
|
-
|
|
7706
|
+
writeFileSync3(join4(dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
7601
7707
|
`);
|
|
7602
7708
|
return r;
|
|
7603
7709
|
};
|
|
@@ -7608,7 +7714,7 @@ async function runOne(opts) {
|
|
|
7608
7714
|
commands: opts.workdirSetup,
|
|
7609
7715
|
cwd: workdir,
|
|
7610
7716
|
env: stepEnv,
|
|
7611
|
-
logPath:
|
|
7717
|
+
logPath: join4(dir, "workdir_setup.txt")
|
|
7612
7718
|
});
|
|
7613
7719
|
if (error)
|
|
7614
7720
|
return done("error", `not graded: ${error}`);
|
|
@@ -7625,6 +7731,13 @@ async function runOne(opts) {
|
|
|
7625
7731
|
await new Promise((res) => stderr.end(res));
|
|
7626
7732
|
if (outcome.sessionId)
|
|
7627
7733
|
r.session = findSession(opts.configDir, outcome.sessionId);
|
|
7734
|
+
writeTranscript({
|
|
7735
|
+
path: r.transcript,
|
|
7736
|
+
title: `${test.name}#${n}`,
|
|
7737
|
+
prompt: r.prompt,
|
|
7738
|
+
events: outcome.events,
|
|
7739
|
+
workdir
|
|
7740
|
+
});
|
|
7628
7741
|
r.num_turns = outcome.result?.num_turns ?? null;
|
|
7629
7742
|
r.cost = outcome.result?.total_cost_usd ?? null;
|
|
7630
7743
|
if (!outcome.ok)
|
|
@@ -7637,7 +7750,11 @@ async function runOne(opts) {
|
|
|
7637
7750
|
r.checks.push(await runShell({
|
|
7638
7751
|
run: render(c.run, opts.vars),
|
|
7639
7752
|
cwd: workdir,
|
|
7640
|
-
env: {
|
|
7753
|
+
env: {
|
|
7754
|
+
...stepEnv,
|
|
7755
|
+
FIREGOAT_SESSION: r.session,
|
|
7756
|
+
FIREGOAT_TRANSCRIPT: r.transcript
|
|
7757
|
+
},
|
|
7641
7758
|
timeoutMs: CHECK_TIMEOUT_MS,
|
|
7642
7759
|
log
|
|
7643
7760
|
}));
|
|
@@ -7697,7 +7814,7 @@ async function evalCommand(args, root) {
|
|
|
7697
7814
|
if (!claudePath)
|
|
7698
7815
|
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.`);
|
|
7699
7816
|
const firegoatDir = ensureFiregoatDir(root);
|
|
7700
|
-
const configDir =
|
|
7817
|
+
const configDir = join4(firegoatDir, ".claude");
|
|
7701
7818
|
mkdirSync2(configDir, { recursive: true });
|
|
7702
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");
|
|
7703
7820
|
const loginProblem = checkLogin({
|
|
@@ -7708,8 +7825,8 @@ async function evalCommand(args, root) {
|
|
|
7708
7825
|
if (loginProblem)
|
|
7709
7826
|
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
7710
7827
|
const runId = newRunId();
|
|
7711
|
-
const runDir =
|
|
7712
|
-
const out =
|
|
7828
|
+
const runDir = join4(firegoatDir, "runs", runId);
|
|
7829
|
+
const out = join4(runDir, "out");
|
|
7713
7830
|
mkdirSync2(out, { recursive: true });
|
|
7714
7831
|
const stepEnv = {
|
|
7715
7832
|
...process.env,
|
|
@@ -7724,7 +7841,7 @@ async function evalCommand(args, root) {
|
|
|
7724
7841
|
commands: config.setup,
|
|
7725
7842
|
cwd: root,
|
|
7726
7843
|
env: stepEnv,
|
|
7727
|
-
logPath:
|
|
7844
|
+
logPath: join4(runDir, "setup.txt")
|
|
7728
7845
|
});
|
|
7729
7846
|
if (error)
|
|
7730
7847
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
@@ -7749,7 +7866,7 @@ async function evalCommand(args, root) {
|
|
|
7749
7866
|
for (const r of results)
|
|
7750
7867
|
totals[r.status]++;
|
|
7751
7868
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
7752
|
-
|
|
7869
|
+
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
7753
7870
|
`);
|
|
7754
7871
|
const cost = results.reduce((sum, r) => sum + (r.cost ?? 0), 0);
|
|
7755
7872
|
say(`
|
|
@@ -7762,8 +7879,8 @@ ${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${resu
|
|
|
7762
7879
|
|
|
7763
7880
|
// src/init.ts
|
|
7764
7881
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7765
|
-
import { existsSync as existsSync3, writeFileSync as
|
|
7766
|
-
import { basename, join as
|
|
7882
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7883
|
+
import { basename, join as join5 } from "node:path";
|
|
7767
7884
|
import { createInterface } from "node:readline/promises";
|
|
7768
7885
|
var SKILLS_ARGV = [
|
|
7769
7886
|
"skills",
|
|
@@ -7867,11 +7984,11 @@ ${INIT_HELP}`);
|
|
|
7867
7984
|
}
|
|
7868
7985
|
}
|
|
7869
7986
|
ensureFiregoatDir(cwd);
|
|
7870
|
-
const configPath =
|
|
7987
|
+
const configPath = join5(cwd, CONFIG_FILE);
|
|
7871
7988
|
if (existsSync3(configPath) && !force)
|
|
7872
7989
|
console.log(`kept ${CONFIG_FILE} (--force replaces it)`);
|
|
7873
7990
|
else {
|
|
7874
|
-
|
|
7991
|
+
writeFileSync4(configPath, template(basename(cwd)));
|
|
7875
7992
|
console.log(`wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
7876
7993
|
}
|
|
7877
7994
|
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|