@garuda-agent/garuda 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/dist/{acpCommand-JAYNMIMP.js → acpCommand-PUJHFWYU.js} +11 -12
  4. package/dist/advise-C777SZ5M.js +433 -0
  5. package/dist/buildInfo-Z4P7QAQL.js +8 -0
  6. package/dist/{chunk-QLBCM2YQ.js → chunk-2FIV2VZP.js} +2 -2
  7. package/dist/{chunk-TLA2PDGM.js → chunk-4BELTWP4.js} +5 -5
  8. package/dist/{chunk-JMH5W5DG.js → chunk-65NUEOI7.js} +44 -1
  9. package/dist/chunk-6JQTPW3P.js +88 -0
  10. package/dist/chunk-A5DLAXB4.js +1780 -0
  11. package/dist/{chunk-DQSCCE77.js → chunk-AX3A5KPH.js} +5 -5
  12. package/dist/chunk-CFDRP6XW.js +9 -0
  13. package/dist/chunk-E6YRCDSF.js +9 -0
  14. package/dist/{chunk-34WBDQBL.js → chunk-HP6EGFHV.js} +18 -18
  15. package/dist/{chunk-GKU62MHQ.js → chunk-LWNMEGDF.js} +2 -2
  16. package/dist/{chunk-5UDYQ5MK.js → chunk-STVFWSIL.js} +5 -7
  17. package/dist/{chunk-IKPGHM54.js → chunk-YEH2OONM.js} +3 -3
  18. package/dist/cli/index.js +39 -40
  19. package/dist/{evalCommand-BLJJ4G76.js → evalCommand-MRO6ENCA.js} +70 -42
  20. package/dist/{evalTune-W53KAD4V.js → evalTune-VL5RHYOG.js} +17 -18
  21. package/dist/{export-E677HIQS.js → export-ZPF3AMO6.js} +5 -5
  22. package/dist/{exportHtml-BMV73YRO.js → exportHtml-ISISG5HG.js} +4 -4
  23. package/dist/{inkChat-EVLYDC3K.js → inkChat-UDFX6GH6.js} +11 -12
  24. package/dist/{manager-L25IUXHW.js → manager-YH7722C2.js} +2 -2
  25. package/dist/runner-EBF6EOYM.js +97 -0
  26. package/dist/{setupCommand-OSOMFHDQ.js → setupCommand-YMF6I3MD.js} +8 -9
  27. package/dist/{tune-VX2WQTHS.js → tune-KSNKBPNJ.js} +8 -9
  28. package/package.json +1 -1
  29. package/dist/advise-HDKJGNBL.js +0 -195
  30. package/dist/buildInfo-K73FFI5S.js +0 -8
  31. package/dist/chunk-CWVXOSAA.js +0 -894
  32. package/dist/chunk-DUYLVGPY.js +0 -821
  33. package/dist/chunk-H7LIBGZT.js +0 -9
  34. package/dist/chunk-IYGJWX65.js +0 -9
  35. package/dist/chunk-NH72AELP.js +0 -48
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.2 (2026-11-06)
4
+
5
+ A fix release for repo evals (`garuda eval -s repo`, `garuda advise`). On macOS, 0.18.1 failed
6
+ every check of a project whose tools live in `~/Library` (pnpm does), and Vite projects failed
7
+ before a test ran. It also makes `garuda advise` faster and cheaper to repeat: runs in parallel,
8
+ reuse of earlier runs, a cost estimate from real requests, and the command to go on after a stop
9
+ at the cap. A provider outage no longer counts against a model.
10
+
11
+ - Fix: repo-task evals (`-s repo`, `garuda advise`) on macOS failed every check in 0.18.1 when the
12
+ project's tools live in `~/Library`. pnpm runs the version that a project asks for from
13
+ `~/Library/pnpm`, and the 0.18.1 rules denied all of the home folder except hidden entries. The
14
+ check stopped with "EPERM: process.cwd failed", so both models passed 0 of 30 in the user's run.
15
+ `~/Library` is readable again in these runs; Mail, Messages, Safari, iCloud Drive, the other
16
+ personal parts and the keychains stay denied for every sandboxed command. Run 0.18.1 repo evals
17
+ again.
18
+ - `advise` gives no advice when neither model passes a task: the check itself may fail.
19
+ - Evals: a timeout while the model's API is silent is a new failure kind, `provider`. It applies
20
+ when a request was open at the limit, and the stream had broken and been retried or the API had
21
+ sent nothing for 2 minutes. Such a run is not scored, and it runs once more by itself; its
22
+ spending is kept. In the user's Sonnet run, 4 of 5 timeouts were such stalls, and they counted
23
+ against the model. A timeout also shows its steps now, not 0.
24
+ - Fix: in repo-task evals, Vite could not write its compiled `vitest.config.ts` to
25
+ `node_modules/.vite-temp`, because the task's `node_modules` links to the project's and was
26
+ read-only. In the user's suite 10 of 30 tasks failed for both models before a test ran.
27
+ Commands may now write `.vite-temp`, `.vite` and `.cache` there, and nothing else.
28
+ - `garuda eval --from-git` checks each kept commit a third time, as an eval runs it (a copy, in the
29
+ sandbox), and leaves out a commit whose tests fail there. Before, it checked only on the host,
30
+ and a task could pass the build but never an eval (2 of 30 in the user's suite: tests that start
31
+ their own sandbox). Build your suite again.
32
+ - The Homebrew formula in the repository names 0.18.1 and its tarball hash.
33
+ - `garuda advise` runs 4 tasks at a time (`--parallel <n>`); each result line starts with the
34
+ model and a count, as in `[haiku-5-5 12/30] ✓ fix-parser: pass`. The first real run, one at a
35
+ time, took 64 minutes for 30 tasks.
36
+ - `advise` reuses an earlier run of a model when the model, the task hashes, `--repeat`, `--seed`
37
+ and the Garuda version and build commit are the same and every run is there. Runs of 0.18.0 and
38
+ before are not reused: they could read your checkout (the 0.18.1 fix).
39
+ - The cost estimate comes from the mean cost of earlier runs of the model on the same tasks. With
40
+ none, it prices each request of the other model's runs (from their session files) at this
41
+ model's prices; else the token guess. Each line says which. (A first version scaled by the ratio
42
+ of list prices: 20 for Sonnet 5.5 over Haiku 5.5, but Haiku costs 5 times as much for a prompt
43
+ over 100 000 tokens. On a real run it estimated $19.16 where the requests give $12.62.)
44
+ - The repriced estimate is scaled by what both models really cost on shared repo tasks before:
45
+ Sonnet 5.5 needs fewer steps and tokens than Haiku 5.5, and cost $6.30 where Haiku's requests at
46
+ its prices gave $12.62 (× 0.52). Without the factor, the user's 90-run estimate was $33.62; with
47
+ it, $17.37. Runs where no task passed, and built-in suites, are left out.
48
+ - When the estimate or the first model's real cost passes `--max-cost`, `advise` prints the
49
+ command that goes on (`garuda advise --models a,b --repeat R --seed S --max-cost N`). The first
50
+ model's run is kept and reused.
51
+ - `advise` stops when a run fails to start (for example a refused key), not at a missing report.
52
+
3
53
  ## 0.18.1 (2026-11-06)
4
54
 
5
55
  A security release. In 0.18.0 and earlier, a command in the OS sandbox could read your personal
package/README.md CHANGED
@@ -7,7 +7,7 @@ Website and docs: https://madhu-sv.github.io/garuda/
7
7
  Garuda is a coding agent for the terminal, and for editors over the Agent Client Protocol. It reads
8
8
  and edits your code, runs commands in an OS sandbox, and keeps an audit log of its permission
9
9
  decisions. It works with Claude and with OpenAI-compatible models, local ones too. This is version
10
- 0.18.1. Release notes: [CHANGELOG.md](CHANGELOG.md).
10
+ 0.18.2. Release notes: [CHANGELOG.md](CHANGELOG.md).
11
11
 
12
12
  ## What Garuda does on your machine
13
13
 
@@ -1,19 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- Runtime,
4
- auditDirFor
5
- } from "./chunk-5UDYQ5MK.js";
6
- import "./chunk-G2AX7MGS.js";
7
- import "./chunk-ULAJPMY7.js";
8
- import "./chunk-JMH5W5DG.js";
9
2
  import {
10
3
  chooseModel,
11
4
  keepProviderKeys,
12
5
  loadModelsConfig,
13
6
  providerKeySource,
14
7
  resolveModel
15
- } from "./chunk-QLBCM2YQ.js";
16
- import "./chunk-NH72AELP.js";
8
+ } from "./chunk-2FIV2VZP.js";
9
+ import {
10
+ Runtime,
11
+ auditDirFor
12
+ } from "./chunk-STVFWSIL.js";
13
+ import "./chunk-G2AX7MGS.js";
14
+ import "./chunk-ULAJPMY7.js";
15
+ import "./chunk-65NUEOI7.js";
17
16
  import "./chunk-4QRTTRVK.js";
18
17
  import "./chunk-A2QAIRQL.js";
19
18
  import {
@@ -26,7 +25,7 @@ import {
26
25
  import "./chunk-MNE7HHX2.js";
27
26
  import {
28
27
  VERSION
29
- } from "./chunk-IYGJWX65.js";
28
+ } from "./chunk-CFDRP6XW.js";
30
29
  import "./chunk-5DEWP3YX.js";
31
30
  import "./chunk-PZS3V6P3.js";
32
31
  import "./chunk-QANRZW7W.js";
@@ -50,7 +49,7 @@ async function acpCommand(options) {
50
49
  `);
51
50
  return 1;
52
51
  }
53
- const { setupCommand, terminalIO } = await import("./setupCommand-OSOMFHDQ.js");
52
+ const { setupCommand, terminalIO } = await import("./setupCommand-YMF6I3MD.js");
54
53
  return setupCommand({}, { io: await terminalIO() });
55
54
  }
56
55
  const output = protocolOutput(process.stdout, process.stderr);
@@ -193,4 +192,4 @@ export {
193
192
  prepare,
194
193
  protocolOutput
195
194
  };
196
- //# sourceMappingURL=acpCommand-JAYNMIMP.js.map
195
+ //# sourceMappingURL=acpCommand-PUJHFWYU.js.map
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ taskHash
4
+ } from "./chunk-6JQTPW3P.js";
5
+ import {
6
+ garudaCommit
7
+ } from "./chunk-YAQ6QLKB.js";
8
+ import {
9
+ escapeHtml
10
+ } from "./chunk-4BELTWP4.js";
11
+ import {
12
+ BUILD_COMMIT
13
+ } from "./chunk-E6YRCDSF.js";
14
+ import {
15
+ compareReports,
16
+ costPerPass,
17
+ formatComparison,
18
+ loadReport,
19
+ repoEvalTasks
20
+ } from "./chunk-A5DLAXB4.js";
21
+ import {
22
+ loadModelsConfig,
23
+ resolveModel
24
+ } from "./chunk-2FIV2VZP.js";
25
+ import "./chunk-AX3A5KPH.js";
26
+ import "./chunk-2OYTNNUJ.js";
27
+ import "./chunk-NXJ4XLB5.js";
28
+ import "./chunk-STVFWSIL.js";
29
+ import "./chunk-G2AX7MGS.js";
30
+ import "./chunk-ULAJPMY7.js";
31
+ import "./chunk-65NUEOI7.js";
32
+ import "./chunk-4QRTTRVK.js";
33
+ import "./chunk-A2QAIRQL.js";
34
+ import {
35
+ newSessionId
36
+ } from "./chunk-NLS7GMF7.js";
37
+ import {
38
+ costOf,
39
+ responseCost
40
+ } from "./chunk-7KBOIPUT.js";
41
+ import "./chunk-B25VL4RU.js";
42
+ import "./chunk-MNE7HHX2.js";
43
+ import {
44
+ VERSION
45
+ } from "./chunk-CFDRP6XW.js";
46
+ import "./chunk-5DEWP3YX.js";
47
+ import "./chunk-PZS3V6P3.js";
48
+ import "./chunk-QANRZW7W.js";
49
+ import "./chunk-ATW7BNIG.js";
50
+ import "./chunk-WK5NE6RY.js";
51
+ import "./chunk-AOGFYUKG.js";
52
+ import "./chunk-QXTHEFKP.js";
53
+ import "./chunk-LMKGSYJC.js";
54
+ import "./chunk-BHZ6G65V.js";
55
+ import "./chunk-P6XWWIEK.js";
56
+ import "./chunk-UIRJOQQS.js";
57
+ import "./chunk-4LFJFDCF.js";
58
+ import "./chunk-3NJNVZTK.js";
59
+
60
+ // src/cli/advise.ts
61
+ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
62
+ import { basename, join } from "path";
63
+ import { createInterface } from "readline/promises";
64
+ var ADVISE_MODELS = ["claude-haiku-5-5", "claude-sonnet-5-5"];
65
+ var ADVISE_REPEAT = 2;
66
+ var ADVISE_MAX_COST = 2;
67
+ var GUESS_INPUT_TOKENS = 6e4;
68
+ var GUESS_OUTPUT_TOKENS = 1500;
69
+ function estimateUsd(price, runs) {
70
+ if (price === void 0) return void 0;
71
+ return runs * (GUESS_INPUT_TOKENS * price.input + GUESS_OUTPUT_TOKENS * price.output) / 1e6;
72
+ }
73
+ function adviceText(a, b, c, tasks) {
74
+ const rough = tasks < 10 ? ` With ${tasks} task${tasks === 1 ? "" : "s"}, this is a rough guide.` : "";
75
+ if (c.verdict === "B passes more")
76
+ return `Advice: ${b} for this repo. It passes more often.${rough}`;
77
+ if (c.verdict === "B passes less")
78
+ return `Advice: ${a} for this repo. It passes more often.${rough}`;
79
+ if (c.rateA === 0 && c.rateB === 0) {
80
+ return `Advice: none. Neither model passed a task, so the check itself may fail: read a run's "reason" in report.json before you trust this.`;
81
+ }
82
+ const ratio = c.costRatio;
83
+ if (ratio === void 0) {
84
+ return `Advice: no clear difference in passes, and the cost could not be compared.${rough}`;
85
+ }
86
+ const times = (x) => `${x.toFixed(1)} times`;
87
+ if (ratio.verdict === "B costs less per accepted change") {
88
+ return `Advice: ${b} for this repo. It passes as often, and ${a} costs ${times(1 / ratio.value)} as much per accepted change.${rough}`;
89
+ }
90
+ if (ratio.verdict === "B costs more per accepted change") {
91
+ return `Advice: ${a} for this repo. It passes as often, and ${b} costs ${times(ratio.value)} as much per accepted change.${rough}`;
92
+ }
93
+ const cheaper = ratio.value <= 1 ? b : a;
94
+ return `Advice: either. No clear difference in passes or in cost; ${cheaper} was cheaper in these runs.${rough}`;
95
+ }
96
+ function adviceHtml(comparison, advice) {
97
+ return [
98
+ "<!doctype html>",
99
+ '<html lang="en"><head><meta charset="utf-8">',
100
+ `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">`,
101
+ "<title>Garuda advise</title>",
102
+ "<style>body{font:15px/1.5 system-ui,sans-serif;max-width:900px;margin:32px auto;padding:0 16px}",
103
+ "pre{font:13px/1.45 ui-monospace,monospace;white-space:pre-wrap}.advice{font-weight:600}</style>",
104
+ "</head><body><h1>Which model for this repository</h1>",
105
+ `<p class="advice">${escapeHtml(advice)}</p>`,
106
+ `<pre>${escapeHtml(comparison)}</pre>`,
107
+ "</body></html>",
108
+ ""
109
+ ].join("\n");
110
+ }
111
+ var ADVISE_PARALLEL = 4;
112
+ function pastRuns(evalsDir) {
113
+ const folders = [];
114
+ try {
115
+ for (const name of readdirSync(evalsDir)) {
116
+ const path = join(evalsDir, name);
117
+ folders.push(path, join(path, "a"), join(path, "b"));
118
+ }
119
+ } catch {
120
+ return [];
121
+ }
122
+ const out = [];
123
+ for (const folder of folders) {
124
+ const file = join(folder, "report.json");
125
+ try {
126
+ const data = JSON.parse(readFileSync(file, "utf8"));
127
+ const garuda = data.garuda;
128
+ out.push({
129
+ path: folder,
130
+ model: typeof data.spec === "string" ? data.spec : typeof data.model === "string" ? data.model : void 0,
131
+ version: typeof garuda?.version === "string" ? garuda.version : void 0,
132
+ commit: typeof garuda?.commit === "string" ? garuda.commit : void 0,
133
+ suite: typeof data.suite === "string" ? data.suite : void 0,
134
+ repeat: typeof data.repeat === "number" ? data.repeat : void 0,
135
+ seed: typeof data.seed === "number" ? data.seed : void 0,
136
+ tasks: typeof data.tasks === "object" && data.tasks !== null ? data.tasks : void 0,
137
+ results: Array.isArray(data.results) ? data.results : [],
138
+ mtimeMs: statSync(file).mtimeMs
139
+ });
140
+ } catch {
141
+ }
142
+ }
143
+ return out.sort((x, y) => y.mtimeMs - x.mtimeMs);
144
+ }
145
+ var sameTasks = (a, b) => a !== void 0 && Object.keys(a).length === Object.keys(b).length && Object.entries(b).every(([id, hash]) => a[id] === hash);
146
+ function reusableRun(runs, want) {
147
+ const count = Object.keys(want.tasks).length * want.repeat;
148
+ return runs.find(
149
+ (r) => r.model === want.model && r.version === want.version && r.commit === want.commit && r.suite === "repo" && r.repeat === want.repeat && r.seed === want.seed && sameTasks(r.tasks, want.tasks) && r.results.length === count
150
+ );
151
+ }
152
+ function pastCostPerRun(runs, model, tasks) {
153
+ const results = runs.filter((r) => r.model === model && sameTasks(r.tasks, tasks)).flatMap((r) => r.results);
154
+ if (results.length === 0 || results.some((r) => r.costUsd === void 0)) return void 0;
155
+ return results.reduce((sum, r) => sum + (r.costUsd ?? 0), 0) / results.length;
156
+ }
157
+ function repricedSessionCost(file, price) {
158
+ let text;
159
+ try {
160
+ text = readFileSync(file, "utf8");
161
+ } catch {
162
+ return void 0;
163
+ }
164
+ let usd = 0;
165
+ for (const line of text.split("\n")) {
166
+ if (line.trim() === "") continue;
167
+ let record;
168
+ try {
169
+ record = JSON.parse(line);
170
+ } catch {
171
+ return void 0;
172
+ }
173
+ if (record.type === "unreported" || record.type === "child_debit") return void 0;
174
+ if (line.includes('"subagent":{')) return void 0;
175
+ if (record.type === "assistant") {
176
+ const cost = responseCost(record.response, price);
177
+ if (cost === void 0) return void 0;
178
+ usd += cost;
179
+ }
180
+ if (record.type === "side_usage") usd += costOf(record.usage, price);
181
+ }
182
+ return usd;
183
+ }
184
+ function repricedCostPerRun(runs, model, tasks, price) {
185
+ if (price === void 0) return void 0;
186
+ const costs = [];
187
+ for (const run of runs.filter((r) => r.model === model && sameTasks(r.tasks, tasks))) {
188
+ for (const result of run.results) {
189
+ const file = result.sessionFile;
190
+ if (file === void 0) {
191
+ costs.push(void 0);
192
+ continue;
193
+ }
194
+ const near = join(run.path, basename(file));
195
+ costs.push(repricedSessionCost(near, price) ?? repricedSessionCost(file, price));
196
+ }
197
+ }
198
+ const known = costs.filter((c) => c !== void 0);
199
+ if (known.length === 0 || known.length * 2 < costs.length) return void 0;
200
+ return known.reduce((sum, c) => sum + c, 0) / known.length;
201
+ }
202
+ function repriceFactor(runs, source, target, price) {
203
+ const useful = runs.filter((r) => r.suite === "repo" && r.results.some((x) => x.passed));
204
+ const sets = [];
205
+ for (const run of useful.filter((r) => r.model === source && r.tasks !== void 0)) {
206
+ const tasks = run.tasks;
207
+ if (sets.some((t) => sameTasks(t, tasks))) continue;
208
+ if (useful.some((r) => r.model === target && sameTasks(r.tasks, tasks))) sets.push(tasks);
209
+ }
210
+ const ratios = [];
211
+ for (const tasks of sets) {
212
+ const real = pastCostPerRun(useful, target, tasks);
213
+ const repriced = repricedCostPerRun(useful, source, tasks, price);
214
+ if (real !== void 0 && repriced !== void 0 && repriced > 0) ratios.push(real / repriced);
215
+ }
216
+ if (ratios.length === 0) return void 0;
217
+ return { value: ratios.reduce((sum, r) => sum + r, 0) / ratios.length, sets: ratios.length };
218
+ }
219
+ function costEstimate(runs, own, repriced, price) {
220
+ if (own !== void 0) return { usd: own * runs, how: "from earlier runs" };
221
+ if (repriced.perRun !== void 0) {
222
+ const f = repriced.factor;
223
+ return {
224
+ usd: repriced.perRun * (f?.value ?? 1) * runs,
225
+ how: f === void 0 ? `from ${repriced.from}'s requests at this model's prices` : `from ${repriced.from}'s requests at this model's prices, \xD7 ${f.value.toFixed(2)} as in ${f.sets} earlier comparison${f.sets === 1 ? "" : "s"}`
226
+ };
227
+ }
228
+ return { usd: estimateUsd(price, runs), how: "a guess" };
229
+ }
230
+ var sumKnown = (values) => values.some((v) => v === void 0) ? void 0 : values.reduce((sum, v) => sum + (v ?? 0), 0);
231
+ function continueCommand(a, b, repeat, seed, maxCost) {
232
+ return `garuda advise --models ${a},${b} --repeat ${repeat} --seed ${seed} --max-cost ${Math.ceil(maxCost)}`;
233
+ }
234
+ async function runAdvise(options) {
235
+ const [a, b] = options.models ?? ADVISE_MODELS;
236
+ if (a === void 0 || b === void 0 || options.models?.length === 1 || (options.models?.length ?? 2) > 2) {
237
+ process.stderr.write("Give two models: --models <a>,<b>.\n");
238
+ return 1;
239
+ }
240
+ const repeat = options.repeat ?? ADVISE_REPEAT;
241
+ const maxCost = options.maxCost ?? ADVISE_MAX_COST;
242
+ const seed = options.seed ?? 1;
243
+ const parallel = options.parallel ?? ADVISE_PARALLEL;
244
+ if (!Number.isInteger(repeat) || repeat < 1 || !(maxCost > 0) || !Number.isInteger(parallel) || parallel < 1) {
245
+ process.stderr.write(
246
+ "--repeat and --parallel must be whole numbers of 1 or more, and --max-cost above 0.\n"
247
+ );
248
+ return 1;
249
+ }
250
+ const { repoSuite, runEvalCommand } = await import("./evalCommand-MRO6ENCA.js");
251
+ const suite = await repoSuite();
252
+ if (typeof suite === "string") {
253
+ process.stderr.write(`${suite}
254
+ `);
255
+ return 1;
256
+ }
257
+ const repoTasks = repoEvalTasks(suite);
258
+ const tasks = repoTasks.length;
259
+ const hashes = Object.fromEntries(repoTasks.map((t) => [t.id, taskHash(t)]));
260
+ const models = await loadModelsConfig();
261
+ if (models.problem !== void 0) {
262
+ process.stderr.write(`${models.problem}
263
+ `);
264
+ return 1;
265
+ }
266
+ let prices;
267
+ try {
268
+ prices = [resolveModel(a, models.config).info.price, resolveModel(b, models.config).info.price];
269
+ } catch (error) {
270
+ process.stderr.write(`${error.message}
271
+ `);
272
+ return 1;
273
+ }
274
+ const evalsDir = join(process.cwd(), ".garuda", "evals");
275
+ const past = pastRuns(evalsDir);
276
+ const commit = BUILD_COMMIT ?? await garudaCommit();
277
+ const want = { repeat, seed, tasks: hashes, version: VERSION, commit };
278
+ const reuse = [
279
+ reusableRun(past, { ...want, model: a }),
280
+ reusableRun(past, { ...want, model: b })
281
+ ];
282
+ const runs = tasks * repeat;
283
+ const perRun = [pastCostPerRun(past, a, hashes), pastCostPerRun(past, b, hashes)];
284
+ const repriced = (i, from) => perRun[i] === void 0 ? repricedCostPerRun(past, from, hashes, prices[i]) : void 0;
285
+ const factors = [
286
+ perRun[0] === void 0 ? repriceFactor(past, b, a, prices[0]) : void 0,
287
+ perRun[1] === void 0 ? repriceFactor(past, a, b, prices[1]) : void 0
288
+ ];
289
+ const estimates = [
290
+ costEstimate(
291
+ runs,
292
+ perRun[0],
293
+ { perRun: repriced(0, b), from: b, factor: factors[0] },
294
+ prices[0]
295
+ ),
296
+ costEstimate(
297
+ runs,
298
+ perRun[1],
299
+ { perRun: repriced(1, a), from: a, factor: factors[1] },
300
+ prices[1]
301
+ )
302
+ ];
303
+ const line = (model, i) => {
304
+ const r = reuse[i];
305
+ const e = estimates[i];
306
+ return r !== void 0 ? ` ${model}: reuses ${r.path} (Garuda ${r.version}, ${r.results.length} runs)` : ` ${model}: ${runs} runs, ${e?.usd === void 0 ? "cost unknown" : `about $${e.usd.toFixed(2)} (${e.how})`}`;
307
+ };
308
+ const toSpend = sumKnown(
309
+ [0, 1].filter((i) => reuse[i] === void 0).map((i) => estimates[i]?.usd)
310
+ );
311
+ process.stderr.write(
312
+ [
313
+ `Your repo: ${tasks} task${tasks === 1 ? "" : "s"} from its history (tests that fail before the commit and pass after it).`,
314
+ `${repeat} run${repeat === 1 ? "" : "s"} a task, ${parallel} at a time, seed ${seed}:`,
315
+ line(a, 0),
316
+ line(b, 1),
317
+ `Cap: --max-cost $${maxCost}; it stops before a model that would pass it.`,
318
+ ""
319
+ ].join("\n")
320
+ );
321
+ if (toSpend !== void 0 && toSpend > maxCost) {
322
+ process.stderr.write(
323
+ `The estimate passes the cap. To go on: ${continueCommand(a, b, repeat, seed, toSpend * 1.25)}
324
+ `
325
+ );
326
+ return 1;
327
+ }
328
+ if (options.yes !== true && (reuse[0] === void 0 || reuse[1] === void 0)) {
329
+ if (!process.stdin.isTTY) {
330
+ process.stderr.write("Add --yes to start without a question.\n");
331
+ return 1;
332
+ }
333
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
334
+ const answer = await rl.question("Start? (y/N) ");
335
+ rl.close();
336
+ if (!/^y(es)?$/i.test(answer.trim())) return 1;
337
+ }
338
+ const dir = join(evalsDir, `advise-${newSessionId()}`);
339
+ mkdirSync(dir, { recursive: true });
340
+ const runModel = async (model, label, i) => {
341
+ const earlier = reuse[i];
342
+ if (earlier !== void 0) {
343
+ process.stderr.write(`Reusing ${earlier.path} for ${model}.
344
+ `);
345
+ return earlier.path;
346
+ }
347
+ const outDir = join(dir, label);
348
+ const code = await runEvalCommand({
349
+ model,
350
+ suite: "repo",
351
+ repeat,
352
+ shuffle: true,
353
+ seed,
354
+ parallel,
355
+ outDir,
356
+ progress: model.replace(/^claude-/, "")
357
+ });
358
+ return code === 1 ? void 0 : outDir;
359
+ };
360
+ const pathA = await runModel(a, "a", 0);
361
+ if (pathA === void 0) return 1;
362
+ const first = await loadReport(pathA);
363
+ if (reuse[0] === void 0 && reuse[1] === void 0) {
364
+ const spent = costPerPass(first.results);
365
+ const spentA = spent.kind === "unknown" ? void 0 : spent.totalUsd;
366
+ const nextB = costEstimate(
367
+ runs,
368
+ perRun[1],
369
+ {
370
+ perRun: repricedCostPerRun(pastRuns(evalsDir), a, hashes, prices[1]),
371
+ from: a,
372
+ factor: factors[1]
373
+ },
374
+ prices[1]
375
+ ).usd;
376
+ if (spentA !== void 0 && nextB !== void 0 && spentA + nextB > maxCost) {
377
+ process.stderr.write(
378
+ [
379
+ `Stopped before ${b}: ${a} cost $${spentA.toFixed(2)}, and ${b} would cost about $${nextB.toFixed(2)}, past $${maxCost}.`,
380
+ `${a}'s run is kept and will be reused. To go on: ${continueCommand(a, b, repeat, seed, nextB * 1.25)}`,
381
+ ""
382
+ ].join("\n")
383
+ );
384
+ return 1;
385
+ }
386
+ }
387
+ const pathB = await runModel(b, "b", 1);
388
+ if (pathB === void 0) return 1;
389
+ const second = await loadReport(pathB);
390
+ const result = compareReports(first, second, { seed });
391
+ if ("problems" in result) {
392
+ process.stderr.write(`The two runs cannot be compared:
393
+ ${result.problems.join("\n")}
394
+ `);
395
+ return 1;
396
+ }
397
+ const comparison = formatComparison(first, second, result.comparison);
398
+ const advice = adviceText(a, b, result.comparison, tasks);
399
+ writeFileSync(join(dir, "advice.txt"), `${advice}
400
+
401
+ ${comparison}
402
+ `);
403
+ writeFileSync(join(dir, "report.html"), adviceHtml(comparison, advice));
404
+ process.stdout.write(`
405
+ ${comparison}
406
+
407
+ ${advice}
408
+ Report: ${join(dir, "report.html")}
409
+ `);
410
+ return 0;
411
+ }
412
+ export {
413
+ ADVISE_MAX_COST,
414
+ ADVISE_MODELS,
415
+ ADVISE_PARALLEL,
416
+ ADVISE_REPEAT,
417
+ GUESS_INPUT_TOKENS,
418
+ GUESS_OUTPUT_TOKENS,
419
+ adviceHtml,
420
+ adviceText,
421
+ continueCommand,
422
+ costEstimate,
423
+ estimateUsd,
424
+ pastCostPerRun,
425
+ pastRuns,
426
+ repriceFactor,
427
+ repricedCostPerRun,
428
+ repricedSessionCost,
429
+ reusableRun,
430
+ runAdvise,
431
+ sumKnown
432
+ };
433
+ //# sourceMappingURL=advise-C777SZ5M.js.map
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BUILD_COMMIT
4
+ } from "./chunk-E6YRCDSF.js";
5
+ export {
6
+ BUILD_COMMIT
7
+ };
8
+ //# sourceMappingURL=buildInfo-Z4P7QAQL.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeFileAtomic
4
- } from "./chunk-NH72AELP.js";
4
+ } from "./chunk-65NUEOI7.js";
5
5
  import {
6
6
  keepSecretForRedaction,
7
7
  lookupModel
@@ -332,4 +332,4 @@ export {
332
332
  providerKeyValue,
333
333
  resolveModel
334
334
  };
335
- //# sourceMappingURL=chunk-QLBCM2YQ.js.map
335
+ //# sourceMappingURL=chunk-2FIV2VZP.js.map
@@ -2,7 +2,10 @@
2
2
  import {
3
3
  summariseCall,
4
4
  summariseResult
5
- } from "./chunk-DQSCCE77.js";
5
+ } from "./chunk-AX3A5KPH.js";
6
+ import {
7
+ receiptText
8
+ } from "./chunk-2OYTNNUJ.js";
6
9
  import {
7
10
  hasReceipt
8
11
  } from "./chunk-4QRTTRVK.js";
@@ -10,9 +13,6 @@ import {
10
13
  serverCallText,
11
14
  serverResultSummary
12
15
  } from "./chunk-A2QAIRQL.js";
13
- import {
14
- receiptText
15
- } from "./chunk-2OYTNNUJ.js";
16
16
 
17
17
  // src/cli/exportHtml.ts
18
18
  function escapeHtml(text) {
@@ -153,4 +153,4 @@ export {
153
153
  callChange,
154
154
  sessionHtml
155
155
  };
156
- //# sourceMappingURL=chunk-TLA2PDGM.js.map
156
+ //# sourceMappingURL=chunk-4BELTWP4.js.map
@@ -91,10 +91,53 @@ function prefixInfo(id) {
91
91
  return best?.info;
92
92
  }
93
93
 
94
+ // src/tools/atomicWrite.ts
95
+ import { randomBytes } from "crypto";
96
+ import { chmod, link, realpath, rename, stat, unlink, writeFile as writeFile2 } from "fs/promises";
97
+ import { basename, dirname as dirname2, join as join2 } from "path";
98
+ async function writeFileAtomic(path, content, { createOnly }) {
99
+ const target = createOnly ? path : await realpath(path);
100
+ const temp = join2(
101
+ dirname2(target),
102
+ `.${basename(target)}.garuda-${randomBytes(4).toString("hex")}.tmp`
103
+ );
104
+ const mode = createOnly ? void 0 : (await stat(target)).mode & 4095;
105
+ await writeFile2(temp, content, { flag: "wx", ...mode === void 0 ? {} : { mode } });
106
+ try {
107
+ if (createOnly) {
108
+ try {
109
+ await link(temp, target);
110
+ } catch (error) {
111
+ const code = error.code;
112
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV") throw error;
113
+ if (await exists(target)) throw Object.assign(new Error("EEXIST"), { code: "EEXIST" });
114
+ await rename(temp, target);
115
+ return;
116
+ }
117
+ } else {
118
+ await chmod(temp, mode ?? 420);
119
+ await rename(temp, target);
120
+ return;
121
+ }
122
+ } finally {
123
+ await unlink(temp).catch(() => {
124
+ });
125
+ }
126
+ }
127
+ async function exists(path) {
128
+ try {
129
+ await stat(path);
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
94
136
  export {
137
+ writeFileAtomic,
95
138
  fetchCatalog,
96
139
  saveCatalog,
97
140
  loadCatalog,
98
141
  mergedModels
99
142
  };
100
- //# sourceMappingURL=chunk-JMH5W5DG.js.map
143
+ //# sourceMappingURL=chunk-65NUEOI7.js.map