firegoat 0.9.4 → 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 +150 -36
- 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
|
};
|
|
@@ -7255,6 +7256,7 @@ async function runClaude(opts) {
|
|
|
7255
7256
|
const said = stdout.trim().split(`
|
|
7256
7257
|
`)[0]?.slice(0, 300);
|
|
7257
7258
|
return {
|
|
7259
|
+
events,
|
|
7258
7260
|
ok: false,
|
|
7259
7261
|
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7260
7262
|
};
|
|
@@ -7263,6 +7265,7 @@ async function runClaude(opts) {
|
|
|
7263
7265
|
const result = events.findLast((e) => e?.type === "result");
|
|
7264
7266
|
if (exited.timedOut)
|
|
7265
7267
|
return {
|
|
7268
|
+
events,
|
|
7266
7269
|
ok: false,
|
|
7267
7270
|
reason: `timed out after ${opts.timeoutMs / 1000}s`,
|
|
7268
7271
|
result,
|
|
@@ -7270,18 +7273,20 @@ async function runClaude(opts) {
|
|
|
7270
7273
|
};
|
|
7271
7274
|
if (!result)
|
|
7272
7275
|
return {
|
|
7276
|
+
events,
|
|
7273
7277
|
ok: false,
|
|
7274
7278
|
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7275
7279
|
sessionId
|
|
7276
7280
|
};
|
|
7277
7281
|
if (result.is_error === true || result.subtype !== "success")
|
|
7278
7282
|
return {
|
|
7283
|
+
events,
|
|
7279
7284
|
ok: false,
|
|
7280
7285
|
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7281
7286
|
result,
|
|
7282
7287
|
sessionId
|
|
7283
7288
|
};
|
|
7284
|
-
return { ok: true, result, sessionId };
|
|
7289
|
+
return { events, ok: true, result, sessionId };
|
|
7285
7290
|
}
|
|
7286
7291
|
async function runShell(opts) {
|
|
7287
7292
|
opts.log.write(`$ ${opts.run}
|
|
@@ -7397,8 +7402,10 @@ firegoat.yaml reference:
|
|
|
7397
7402
|
|
|
7398
7403
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7399
7404
|
.firegoat/fixtures/ and copy them in setup or workdir_setup. setup, workdir_setup and checks get the vars as env vars,
|
|
7400
|
-
plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR
|
|
7401
|
-
|
|
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).
|
|
7402
7409
|
|
|
7403
7410
|
Example:
|
|
7404
7411
|
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -7412,9 +7419,9 @@ Example:
|
|
|
7412
7419
|
A script sends one event with the SDK's client (not raw HTTP) and prints its
|
|
7413
7420
|
id, without installing acme from the registry.
|
|
7414
7421
|
checks:
|
|
7415
|
-
- run: grep -qE 'evt_[A-Za-z0-9]+' "$
|
|
7422
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
7416
7423
|
- run: |
|
|
7417
|
-
! grep -Eq 'npm (install|i)
|
|
7424
|
+
! grep -Eq '$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7418
7425
|
|
|
7419
7426
|
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7420
7427
|
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json]
|
|
@@ -7440,12 +7447,15 @@ Output: .firegoat/runs/<runId>/
|
|
|
7440
7447
|
summary.json every run, plus totals
|
|
7441
7448
|
<test>/<n>/run.json one run: status and reason (from the checks), prompt,
|
|
7442
7449
|
end_goal, checks [{run, code}], and paths: workdir,
|
|
7443
|
-
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
|
|
7444
7454
|
setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
|
|
7445
7455
|
|
|
7446
7456
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
7447
|
-
yours to judge
|
|
7448
|
-
|
|
7457
|
+
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
7458
|
+
(often 50x larger).
|
|
7449
7459
|
|
|
7450
7460
|
Examples:
|
|
7451
7461
|
npx firegoat eval --json all tests, JSON summary on stdout
|
|
@@ -7479,14 +7489,106 @@ async function runSteps(opts) {
|
|
|
7479
7489
|
}
|
|
7480
7490
|
}
|
|
7481
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
|
+
|
|
7482
7584
|
// src/which.ts
|
|
7483
|
-
import { accessSync, constants, statSync } from "node:fs";
|
|
7484
|
-
import { delimiter, join as
|
|
7585
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
7586
|
+
import { delimiter, join as join3, resolve } from "node:path";
|
|
7485
7587
|
function resolveBin(bin) {
|
|
7486
7588
|
const executable = (p) => {
|
|
7487
7589
|
try {
|
|
7488
7590
|
accessSync(p, constants.X_OK);
|
|
7489
|
-
return
|
|
7591
|
+
return statSync2(p).isFile();
|
|
7490
7592
|
} catch {
|
|
7491
7593
|
return false;
|
|
7492
7594
|
}
|
|
@@ -7494,8 +7596,8 @@ function resolveBin(bin) {
|
|
|
7494
7596
|
if (bin.includes("/"))
|
|
7495
7597
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7496
7598
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
7497
|
-
if (dir && executable(
|
|
7498
|
-
return
|
|
7599
|
+
if (dir && executable(join3(dir, bin)))
|
|
7600
|
+
return join3(dir, bin);
|
|
7499
7601
|
}
|
|
7500
7602
|
return;
|
|
7501
7603
|
}
|
|
@@ -7550,11 +7652,11 @@ function parseArgs(args) {
|
|
|
7550
7652
|
return opts;
|
|
7551
7653
|
}
|
|
7552
7654
|
function findSession(configDir, sessionId) {
|
|
7553
|
-
const projects =
|
|
7655
|
+
const projects = join4(configDir, "projects");
|
|
7554
7656
|
if (!existsSync2(projects))
|
|
7555
7657
|
return null;
|
|
7556
|
-
for (const d of
|
|
7557
|
-
const f =
|
|
7658
|
+
for (const d of readdirSync2(projects)) {
|
|
7659
|
+
const f = join4(projects, d, `${sessionId}.jsonl`);
|
|
7558
7660
|
if (existsSync2(f))
|
|
7559
7661
|
return f;
|
|
7560
7662
|
}
|
|
@@ -7576,8 +7678,8 @@ function newRunId() {
|
|
|
7576
7678
|
async function runOne(opts) {
|
|
7577
7679
|
const { test, n } = opts;
|
|
7578
7680
|
const started = Date.now();
|
|
7579
|
-
const dir =
|
|
7580
|
-
const workdir =
|
|
7681
|
+
const dir = join4(opts.runDir, test.name, String(n));
|
|
7682
|
+
const workdir = join4(dir, "work");
|
|
7581
7683
|
mkdirSync2(workdir, { recursive: true });
|
|
7582
7684
|
const r = {
|
|
7583
7685
|
test: test.name,
|
|
@@ -7590,8 +7692,9 @@ async function runOne(opts) {
|
|
|
7590
7692
|
dir,
|
|
7591
7693
|
workdir,
|
|
7592
7694
|
session: null,
|
|
7593
|
-
|
|
7594
|
-
|
|
7695
|
+
transcript: join4(dir, "transcript.txt"),
|
|
7696
|
+
stderr: join4(dir, "stderr.txt"),
|
|
7697
|
+
checksLog: join4(dir, "checks.txt"),
|
|
7595
7698
|
num_turns: null,
|
|
7596
7699
|
cost: null,
|
|
7597
7700
|
duration_ms: 0
|
|
@@ -7600,7 +7703,7 @@ async function runOne(opts) {
|
|
|
7600
7703
|
r.status = status;
|
|
7601
7704
|
r.reason = reason;
|
|
7602
7705
|
r.duration_ms = Date.now() - started;
|
|
7603
|
-
|
|
7706
|
+
writeFileSync3(join4(dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
7604
7707
|
`);
|
|
7605
7708
|
return r;
|
|
7606
7709
|
};
|
|
@@ -7611,7 +7714,7 @@ async function runOne(opts) {
|
|
|
7611
7714
|
commands: opts.workdirSetup,
|
|
7612
7715
|
cwd: workdir,
|
|
7613
7716
|
env: stepEnv,
|
|
7614
|
-
logPath:
|
|
7717
|
+
logPath: join4(dir, "workdir_setup.txt")
|
|
7615
7718
|
});
|
|
7616
7719
|
if (error)
|
|
7617
7720
|
return done("error", `not graded: ${error}`);
|
|
@@ -7628,6 +7731,13 @@ async function runOne(opts) {
|
|
|
7628
7731
|
await new Promise((res) => stderr.end(res));
|
|
7629
7732
|
if (outcome.sessionId)
|
|
7630
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
|
+
});
|
|
7631
7741
|
r.num_turns = outcome.result?.num_turns ?? null;
|
|
7632
7742
|
r.cost = outcome.result?.total_cost_usd ?? null;
|
|
7633
7743
|
if (!outcome.ok)
|
|
@@ -7640,7 +7750,11 @@ async function runOne(opts) {
|
|
|
7640
7750
|
r.checks.push(await runShell({
|
|
7641
7751
|
run: render(c.run, opts.vars),
|
|
7642
7752
|
cwd: workdir,
|
|
7643
|
-
env: {
|
|
7753
|
+
env: {
|
|
7754
|
+
...stepEnv,
|
|
7755
|
+
FIREGOAT_SESSION: r.session,
|
|
7756
|
+
FIREGOAT_TRANSCRIPT: r.transcript
|
|
7757
|
+
},
|
|
7644
7758
|
timeoutMs: CHECK_TIMEOUT_MS,
|
|
7645
7759
|
log
|
|
7646
7760
|
}));
|
|
@@ -7700,7 +7814,7 @@ async function evalCommand(args, root) {
|
|
|
7700
7814
|
if (!claudePath)
|
|
7701
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.`);
|
|
7702
7816
|
const firegoatDir = ensureFiregoatDir(root);
|
|
7703
|
-
const configDir =
|
|
7817
|
+
const configDir = join4(firegoatDir, ".claude");
|
|
7704
7818
|
mkdirSync2(configDir, { recursive: true });
|
|
7705
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");
|
|
7706
7820
|
const loginProblem = checkLogin({
|
|
@@ -7711,8 +7825,8 @@ async function evalCommand(args, root) {
|
|
|
7711
7825
|
if (loginProblem)
|
|
7712
7826
|
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
7713
7827
|
const runId = newRunId();
|
|
7714
|
-
const runDir =
|
|
7715
|
-
const out =
|
|
7828
|
+
const runDir = join4(firegoatDir, "runs", runId);
|
|
7829
|
+
const out = join4(runDir, "out");
|
|
7716
7830
|
mkdirSync2(out, { recursive: true });
|
|
7717
7831
|
const stepEnv = {
|
|
7718
7832
|
...process.env,
|
|
@@ -7727,7 +7841,7 @@ async function evalCommand(args, root) {
|
|
|
7727
7841
|
commands: config.setup,
|
|
7728
7842
|
cwd: root,
|
|
7729
7843
|
env: stepEnv,
|
|
7730
|
-
logPath:
|
|
7844
|
+
logPath: join4(runDir, "setup.txt")
|
|
7731
7845
|
});
|
|
7732
7846
|
if (error)
|
|
7733
7847
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
@@ -7752,7 +7866,7 @@ async function evalCommand(args, root) {
|
|
|
7752
7866
|
for (const r of results)
|
|
7753
7867
|
totals[r.status]++;
|
|
7754
7868
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
7755
|
-
|
|
7869
|
+
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
7756
7870
|
`);
|
|
7757
7871
|
const cost = results.reduce((sum, r) => sum + (r.cost ?? 0), 0);
|
|
7758
7872
|
say(`
|
|
@@ -7765,8 +7879,8 @@ ${totals.pass} passed, ${totals.fail} failed, ${totals.error} not graded (${resu
|
|
|
7765
7879
|
|
|
7766
7880
|
// src/init.ts
|
|
7767
7881
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7768
|
-
import { existsSync as existsSync3, writeFileSync as
|
|
7769
|
-
import { basename, join as
|
|
7882
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7883
|
+
import { basename, join as join5 } from "node:path";
|
|
7770
7884
|
import { createInterface } from "node:readline/promises";
|
|
7771
7885
|
var SKILLS_ARGV = [
|
|
7772
7886
|
"skills",
|
|
@@ -7870,11 +7984,11 @@ ${INIT_HELP}`);
|
|
|
7870
7984
|
}
|
|
7871
7985
|
}
|
|
7872
7986
|
ensureFiregoatDir(cwd);
|
|
7873
|
-
const configPath =
|
|
7987
|
+
const configPath = join5(cwd, CONFIG_FILE);
|
|
7874
7988
|
if (existsSync3(configPath) && !force)
|
|
7875
7989
|
console.log(`kept ${CONFIG_FILE} (--force replaces it)`);
|
|
7876
7990
|
else {
|
|
7877
|
-
|
|
7991
|
+
writeFileSync4(configPath, template(basename(cwd)));
|
|
7878
7992
|
console.log(`wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
7879
7993
|
}
|
|
7880
7994
|
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|