@garuda-agent/garuda 0.17.0 → 0.18.1

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 (64) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +3 -2
  3. package/SECURITY.md +1 -1
  4. package/dist/{acpCommand-7EY3NHPG.js → acpCommand-JAYNMIMP.js} +19 -16
  5. package/dist/advise-HDKJGNBL.js +195 -0
  6. package/dist/brief-RWKESOPT.js +91 -0
  7. package/dist/buildCheck-HB46NWLX.js +19 -0
  8. package/dist/buildInfo-K73FFI5S.js +8 -0
  9. package/dist/chunk-2OYTNNUJ.js +216 -0
  10. package/dist/{chunk-Z66BWTMD.js → chunk-34WBDQBL.js} +135 -78
  11. package/dist/chunk-4QRTTRVK.js +88 -0
  12. package/dist/{chunk-2DYMJMRQ.js → chunk-5UDYQ5MK.js} +405 -70
  13. package/dist/{chunk-J4346IDH.js → chunk-7KBOIPUT.js} +2 -1
  14. package/dist/chunk-CWVXOSAA.js +894 -0
  15. package/dist/{chunk-FARS4R67.js → chunk-DQSCCE77.js} +18 -1
  16. package/dist/chunk-DUYLVGPY.js +821 -0
  17. package/dist/chunk-GKU62MHQ.js +5258 -0
  18. package/dist/chunk-H7LIBGZT.js +9 -0
  19. package/dist/chunk-IKPGHM54.js +334 -0
  20. package/dist/chunk-IYGJWX65.js +9 -0
  21. package/dist/{chunk-U65DQZDB.js → chunk-JMH5W5DG.js} +2 -45
  22. package/dist/chunk-L55VUSX3.js +54 -0
  23. package/dist/{chunk-B4SJY7KT.js → chunk-LMKGSYJC.js} +77 -8
  24. package/dist/{chunk-23FXBNBB.js → chunk-MNE7HHX2.js} +2 -1
  25. package/dist/chunk-NH72AELP.js +48 -0
  26. package/dist/{chunk-735BKABA.js → chunk-NLS7GMF7.js} +12 -106
  27. package/dist/{chunk-ZAE25JM2.js → chunk-QANRZW7W.js} +8 -1
  28. package/dist/{chunk-JRVWH27M.js → chunk-QLBCM2YQ.js} +7 -3
  29. package/dist/{chunk-6XHON3Q6.js → chunk-QXTHEFKP.js} +3 -3
  30. package/dist/{chunk-TJIWD7R7.js → chunk-R7RCW4BQ.js} +10 -8
  31. package/dist/chunk-TLA2PDGM.js +156 -0
  32. package/dist/{chunk-5B5HDTUR.js → chunk-TWQ7V2L6.js} +2 -2
  33. package/dist/chunk-ULAJPMY7.js +107 -0
  34. package/dist/chunk-VISKX36R.js +306 -0
  35. package/dist/{chunk-55CFEEIN.js → chunk-WK5NE6RY.js} +2 -2
  36. package/dist/chunk-Y62J7PTD.js +90 -0
  37. package/dist/chunk-YAQ6QLKB.js +40 -0
  38. package/dist/{chunk-7TH3NHCQ.js → chunk-ZKT4P2PC.js} +2 -2
  39. package/dist/cli/index.js +289 -65
  40. package/dist/config-WV2H64TF.js +23 -0
  41. package/dist/{create-W6BV75QX.js → create-OACSGJ37.js} +5 -5
  42. package/dist/evalCommand-BLJJ4G76.js +696 -0
  43. package/dist/evalTune-W53KAD4V.js +302 -0
  44. package/dist/{export-SJ3DUP6W.js → export-E677HIQS.js} +27 -4
  45. package/dist/exportHtml-BMV73YRO.js +21 -0
  46. package/dist/{inkChat-FNUBK43U.js → inkChat-EVLYDC3K.js} +263 -47
  47. package/dist/{install-TKWN5AYH.js → install-4ST5BNRG.js} +3 -3
  48. package/dist/{jobCommand-FE6XN3YD.js → jobCommand-ILOTYOX3.js} +9 -8
  49. package/dist/{launchd-3A3ONEW5.js → launchd-QG6R5RAK.js} +4 -4
  50. package/dist/{lspCommand-ZZ6R4PA2.js → lspCommand-X4FTTONR.js} +3 -3
  51. package/dist/{manager-73MPYFTP.js → manager-L25IUXHW.js} +11 -11
  52. package/dist/{nightCommand-OODJNWDT.js → nightCommand-XPWT6CQZ.js} +9 -8
  53. package/dist/receiptView-YFDHV253.js +19 -0
  54. package/dist/{remove-42WQNVPX.js → remove-STX5UO7X.js} +3 -3
  55. package/dist/{run-ML6GSPN4.js → run-W4TK52WT.js} +6 -6
  56. package/dist/{server-VEYIJHEU.js → server-ASM73QVY.js} +3 -3
  57. package/dist/{setupCommand-GBQIVYJC.js → setupCommand-OSOMFHDQ.js} +10 -9
  58. package/dist/tour-CEVMAC5S.js +18 -0
  59. package/dist/tune-VX2WQTHS.js +52 -0
  60. package/dist/uiState-YFJQ43D7.js +16 -0
  61. package/package.json +1 -1
  62. package/dist/chunk-4JSUUDFQ.js +0 -118
  63. package/dist/chunk-BSK5T2ZG.js +0 -9
  64. package/dist/evalCommand-YVQAWUZE.js +0 -2833
package/CHANGELOG.md CHANGED
@@ -1,5 +1,248 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.1 (2026-11-06)
4
+
5
+ A security release. In 0.18.0 and earlier, a command in the OS sandbox could read your personal
6
+ folders (Documents, Downloads, Pictures and the Photos library, Mail and the like), and on macOS it
7
+ could reach denied paths through `/System/Volumes/Data`. Update to 0.18.1. It also has the UX work
8
+ of docs/lld/ux.md (receipts, a network question, `/rewind`, word-level diffs, the morning brief,
9
+ `/export --html`, `garuda advise` and more) and eval failure kinds.
10
+
11
+ - Security: sandboxed commands can no longer read your personal folders: Desktop, Documents,
12
+ Downloads, Movies, Music, Pictures (with the Photos library), Public, and Mail, Messages, Safari,
13
+ Calendars, Contacts, iCloud Drive and other cloud folders in `~/Library`. A project inside one of
14
+ them stays readable, the rest of that folder does not. On macOS each read and write rule also
15
+ covers the same path under `/System/Volumes/Data`, where a command could reach it before. Found
16
+ in a live run: a model's `find /` in an eval made macOS ask the user to let iTerm into Downloads
17
+ and Photos.
18
+ - Evals: a repo task now runs in a copy of the project with no history, not a git worktree, and
19
+ its commands cannot read the project's checkout (except `node_modules` and venvs) or the rest of
20
+ the home folder except hidden entries. In a live run, a model read the user's checkout, which
21
+ holds the commit that the task asks for. Repo results from before this change may have read the
22
+ answer; run them again.
23
+ - UX, patch 7 (docs/lld/ux.md U11, U8): `/export --html [file]` writes the session as one page:
24
+ your requests, the answers, each tool call that opens to its output (an edit opens to its
25
+ change), and the receipts. Every text is escaped; the page has no script and no link, and its
26
+ own policy forbids them. `garuda advise` runs the repo suite (`garuda eval --from-git`) on two
27
+ models (default Haiku 5.5 and Sonnet 5.5, 2 runs a task, at most $2), compares them as
28
+ `eval compare` does, and advises: the model that passes clearly more, or on a tie the cheaper
29
+ one. It asks first with a cost guess, and writes `advice.txt` and `report.html`.
30
+ - UX, patch 6 (docs/lld/ux.md U7, U9): the morning brief. The first chat after jobs ran shows them
31
+ once: each job's verdict, its tests ("tests 120 → 126" when the runner's summary gives counts)
32
+ and its branch, and the total cost. `.garuda/jobs/brief.seen` keeps the time of the last brief.
33
+ The first-run tour: the first chat in a project with code names its stack ("TypeScript with
34
+ vitest (pnpm)") and offers three first tasks; typing 1, 2 or 3 runs one. It shows once per
35
+ project (`~/.garuda/ui.json` keeps a hash of the folder, not its path).
36
+ - UX, patch 5 (docs/lld/ux.md U5): diffs mark the changed words of a changed line (inverse video),
37
+ in approvals, in hunk review and in `/diff`. From 140 columns a diff is side by side, with the
38
+ line numbers of both files. An approval that can review hunks says so in its first line:
39
+ "3 hunks · h reviews them one by one". Syntax colours are not in this patch (decision D10).
40
+ Only a real diff (with a hunk header) gets the marks and the two columns; other previews, such
41
+ as the project-settings question, keep their simple colours (the first live run showed that
42
+ question twice, side by side).
43
+ - UX, patch 4 (docs/lld/ux.md U6): `/rewind` lists the turns that undo can reach (files, cost, age).
44
+ `/rewind <n>` goes back to the start of turn n in one step, with one question; `/redo` brings the
45
+ turns back one at a time. `/rewind fork <n>` starts a new session with the conversation up to
46
+ turn n and the files as they were then; the old session does not change. A new `fork` record
47
+ starts such a session.
48
+ - UX, patch 3 (docs/lld/ux.md U4): in the chat, calls of one read-only tool in a row become one
49
+ line, for example `● Read 5 files docs/architecture.md, SECURITY.md, +3`; a lone call keeps its
50
+ line and its result. Edits, commands and failed calls always keep a line of their own. Ctrl-O
51
+ still shows the last call's output. The model's plan (`todo_write`) is drawn live above the
52
+ input during the turn and printed once at the end, instead of after every update.
53
+ - The chat's wordmark has a drop shadow (a 3D look), drawn twice as large from 130 columns, with
54
+ the card as wide as the wordmark; a tall screen gets some space above it. A screen reader gets no
55
+ wordmark (the card says the name).
56
+ - The chat starts on a clear screen, with the banner centred, and you type in a visible chat box
57
+ with a hint ("Type a task · / for commands · @ for files"). The old screen goes into the
58
+ scrollback, not away; `"ui": { "clearOnStart": false }` in settings keeps it on screen. Garuda
59
+ stays in the terminal's normal screen, so its scrollback, search and copy still work. The box's
60
+ border dims while a turn runs. With `GARUDA_SCREEN_READER=1`, or under 40 columns, there is no box
61
+ and the banner is not centred.
62
+ - UX, patch 2 (docs/lld/ux.md U2): a chat with no network allowlist starts Garuda's proxy with no
63
+ host on it ("watch mode"). A command in the sandbox that needs a host now asks for it (once, this
64
+ session, or no) instead of failing with a DNS error; an empty list needs no consent. After "this
65
+ session", Garuda says which line in `network.allow` keeps it for the project (a preset such as
66
+ `npm` when the host belongs to one). The receipt names a blocked host. `-p`, jobs and evals keep
67
+ no network, as before.
68
+ - The chat shows your requests with a bar (`▌`) on every line, and Garuda's answer starts with `✦`,
69
+ so requests and answers differ at a glance. With `GARUDA_SCREEN_READER=1` they read "You:" and
70
+ "Garuda:".
71
+ - The chat warns at start when `dist/` is older than its checkout ("run pnpm build first"), as the
72
+ evals do. `garudaCommit` and `staleBuildNote` moved to `src/buildCheck.ts`.
73
+ - Docs: `architecture.md`, `hld.md` and the docs index describe Garuda as it is now. The release
74
+ history in their prose (version lists, "since 0.x", test counts per release) is gone; the
75
+ CHANGELOG keeps it.
76
+ - UX, patch 1 (docs/lld/ux.md): a receipt after each turn that changed files, ran tests, ran a command outside the sandbox, or had a host blocked: the
77
+ files with their lines (from the undo snapshots), the last test run with its count for vitest,
78
+ jest, node:test, pytest, Maven and `go test -v`, the commands in and outside the sandbox, blocked
79
+ hosts, what `/undo` puts back, and the session's cost per kept change. A turn that only answered
80
+ keeps the usage line, also when it ran read-only commands in the sandbox (the first live run: a
81
+ `find` gave a receipt with no news). The session records the receipt (`receipt`), `/export` shows it, and
82
+ `-p --json` has it as `receipt`. Under 60 columns, in a pipe and with `GARUDA_SCREEN_READER=1` it
83
+ is plain lines.
84
+ - The chat asks how to go on after the step limit: go on for as many steps again, summarise what is
85
+ left, or stop. After the same tool call 3 times it says to type a hint, and no longer names
86
+ `--resume`.
87
+ - The chat's footer shows the context as a bar and a number (yellow from 70%, red from 80%). The key
88
+ hints show in the first 5 chats; then the footer says `? keys`, and `?` on an empty line lists them.
89
+ - Design: the UX note (docs/lld/ux.md), with mock-ups and no code yet. A receipt after each
90
+ turn (files, tests, sandbox, undo, cost per kept change), a blocked host as a one-key fix,
91
+ next steps after a stop, a live plan, better diffs, rewind and fork, a morning brief from the
92
+ night shift, `garuda advise` (which model for this repo), a first-run tour, a quieter footer,
93
+ HTML export, and reduced motion and screen-reader modes. Ten decisions for the user.
94
+ - Blog: "Garuda against Claude Code, with intervals": the eval method, the Claude Code
95
+ baseline, the faults the live runs found in the tasks, the Graphify A/B, the tuned prompt, and
96
+ the limits.
97
+ - The Homebrew formula in the repository names 0.18.0 and its tarball hash.
98
+ - Failure kinds: the eval report gives one line, "Failures by kind", that counts the failed runs
99
+ as `check` (the check failed), `protected-files`, `step-limit`, `token-limit`,
100
+ `repeated-calls`, `refusal`, `timeout`, `error` or `setup` (the key, the connection or the MCP
101
+ server), and names up to three runs of each kind. A limit or a timeout wins over the check that
102
+ failed after it. `report.json` has the same in `evidence.failures`, with each failed run's
103
+ steps, tokens and time.
104
+ - The reports of the runs in the blog post are in `docs/quality-baseline/evals/0.18/`, with a
105
+ README that maps each run to the post and gives the `eval compare` commands. Local paths are
106
+ removed. A test checks that the files hold no local path and that `compare` gives the post's
107
+ numbers. The post links to them. Biome and the docs site leave the folder out.
108
+
109
+ ## 0.18.0 (2026-11-04)
110
+
111
+ - Biome 2.6.2 (with @inquirer/prompts 8.7.4, react 19.3.1 and smol-toml 1.9.1, from Dependabot's
112
+ group update). Biome 2.6 formats Markdown and YAML by default: Markdown formatting is off (the
113
+ docs keep their own layout), and pnpm's lockfiles are left out.
114
+ - Dependabot: `@types/node` stays on the oldest supported Node line (no major updates); each other
115
+ major update comes as its own pull request.
116
+ - The Homebrew formula in the repository names 0.17.0 and its tarball hash.
117
+ - Eval evidence, part 1 (T11a): the report gives the pass rate over all attempts with a 95%
118
+ interval (a cluster bootstrap over tasks, so repeats of one task count together), the tasks that
119
+ passed every run or none, the median and p95 of steps, tokens and time, the cost per accepted
120
+ change (all attempts divided by the passes), and unexpected changes (files that a run changed
121
+ but the task did not need; a new `allowChanges` field widens a task's scope). `--shuffle` and
122
+ `--seed` set the run order. `report.json` is version 2, with Garuda's version and commit, the
123
+ model, the OS, the seed, the run order and a hash of each task. A run whose every attempt failed
124
+ with an error gives no interval ("No scored run"), a task with only error runs is not counted as
125
+ "passed none", and an authentication error (401 or 403, for example a refused API key) stops the
126
+ eval at the first run instead of failing every run the same way.
127
+ - `garuda eval compare <a> <b>` (T11a part 2): two reports of the same tasks side by side, the
128
+ difference of the pass rates (B − A) with a paired 95% interval, and each arm's cost per accepted
129
+ change, unexpected changes and median time. It refuses reports whose tasks differ, and a
130
+ difference whose interval contains 0 is "no clear difference". The run report now says when its
131
+ interval is only a rough guide (fewer than 10 tasks, or every draw gave the same rate).
132
+ - The Claude Code baseline (T11b): `garuda eval --agent claude-code` runs `claude -p` on the same
133
+ tasks with the same scratch folders, time limit and grading, so `garuda eval compare` can set
134
+ Garuda against Claude Code. Claude Code runs with every tool allowed, so only in the OS sandbox,
135
+ with network only to `api.anthropic.com`, a new config folder per run, and the API key in its
136
+ environment only. Its turns, tokens and cost come from its JSON result. `eval compare` prints a
137
+ zero difference as "0 pts" and says when every draw gave the same difference. The user's first
138
+ live baseline run found that the proxy for `api.anthropic.com` started only in a Garuda turn, so
139
+ Claude Code had no network: the runner now starts it first (`Runtime.prepareCommands`). An eval
140
+ now also stops at the first run that cannot reach the API (a refused connection, an unknown
141
+ host, a certificate error), as it does for a refused key.
142
+ - `garuda eval compare` also compares the cost per accepted change: the ratio B/A with a paired
143
+ 95% interval, and a verdict ("B costs more", "B costs less" or "no clear difference"). An unknown
144
+ cost gives no ratio. The hard-coupon task allows changes in the coupon module and the API routes
145
+ (`allowChanges`): both agents made them in the first baseline runs. Its task hash changes, so
146
+ compare new hard-suite runs only with new ones.
147
+ - A held-out set (T11c): 10 new, harder tasks in TypeScript, Python and Java, apart from every
148
+ suite. `--split heldout` runs them, `--split all` runs them with the dev tasks, and a plain
149
+ `garuda eval` never does (a held-out id with `-t` is refused with the fix). They cover a defect
150
+ whose cause is in another file (3), a refactor across modules (2), tests that a mutation check
151
+ must find good enough (2), conventions from the repository's `AGENTS.md` that a hidden test
152
+ checks (2), and a vague request (1). Each task's hash is recorded in the tests: a changed task
153
+ must change its hash on purpose, and a tuning result seen before the change is invalid.
154
+ `report.json` records the split. `--list` shows the held-out set. The compare note now says
155
+ "the same pass-rate difference".
156
+ - The first held-out run (Haiku 5.5, 3 runs a task: 27/30) found a fault in one task:
157
+ the export-dates task (now `med-export-dates`) failed 0/3 only because the agent, with the right fix each time, added a CSV
158
+ test to the protected test file. Its hidden test decides, so the visible tests are no longer
159
+ protected, and a new test is in scope there and in the two conventions tasks. The hashes of these
160
+ three tasks change: compare held-out runs only with runs made after this change.
161
+ - A harder held-out set (T11c, second version). The first set was at its ceiling for Haiku 5.5,
162
+ so it is now a dev suite, `-s medium` (ids `med-…`), and 10 new tasks took its place. Each has a
163
+ trap: a quick fix that passes the visible tests fails the hidden test or the mutation check (a
164
+ cache key without the currency, a new idempotency key per retry, invoice shares and yen, a
165
+ command registry with unchanged output, settings by the README's rules, 6 subtle mutants in two
166
+ test-writing tasks, a migration and a reset endpoint by `AGENTS.md`, and a slow search that must
167
+ keep every result). Their ids start with `held-out-`. The tests prove each trap. Compare
168
+ held-out runs only with runs made after this change. The run report's id column now fits the
169
+ longest id, so the columns after it line up.
170
+ - The second held-out run (Haiku 5.5, 3 runs a task: 28/30) found two faults, both in the checks.
171
+ `held-out-conventions-migration` failed 2/3 with correct work: the agent named the field
172
+ `dueAt` (as `created_at` suggests), and the hidden test read `dueDate`, which the prompt never
173
+ named. The prompt now names it. The report gave "The check failed:" with nothing after it: a
174
+ guard stopped the check with no output. Each guard now prints what is missing, and a check with
175
+ no output reports its exit code. Four held-out hashes change.
176
+ - Tests: `test/evals.test.ts` self-tests the basic and hard suites only (the medium tasks ran twice
177
+ there), with 60 s for each task. The user's `pnpm check` timed out on `hard-rounding` at 5 s,
178
+ then on the LSP reference sample. `vitest.config.ts` now gives every test 30 s, the limit that
179
+ the release checks used, so `pnpm check` agrees with them.
180
+ - The third held-out run, with the Claude Code baseline, ran an old build: `dist/` was older than
181
+ the checkout, so the old tasks ran, and `report.json` named the checkout's commit, not the
182
+ build's. A build now records the commit it came from (tsup sets it), `report.json` gives it as
183
+ `garuda.commit` and adds `garuda.checkout` when the checkout differs, and `garuda eval` warns
184
+ at the start: "This build is from commit …, but the checkout is at …: run pnpm build first."
185
+ - Held-out prompts with visible tests now say "You may add tests in new files, but do not change
186
+ the existing test files": in the same run the agent appended a correct test to a protected test
187
+ file after "do not change the existing ones". Five held-out hashes change.
188
+ - Same run, same (old) tasks, Haiku 5.5: Garuda 26/30, Claude Code 27/30, no clear difference in
189
+ the pass rate; Claude Code cost ×1.50 per accepted change (paired interval ×1.35 to ×1.71).
190
+ - Design (T11; T11a and T11b built, T11c–d not yet): eval evidence — intervals and consistency over repeated runs, cost per
191
+ accepted change, unexpected changes, a Claude Code baseline runner, a held-out task split, and a
192
+ proposer loop that edits only a prompt addendum (`docs/lld/eval-evidence.md`).
193
+
194
+ - An eval A/B of one MCP server: `garuda eval --mcp <server>` allows that one server from your
195
+ own `~/.garuda/mcp.json` (no other server, and none from a project file), and
196
+ `--mcp-prepare <command>` runs a command in each scratch folder, in the sandbox, before the agent
197
+ starts; what it writes is not counted as the agent's change. For Graphify:
198
+ `--mcp graphify --mcp-prepare "graphify update ."`. Each run records its MCP tool calls; the
199
+ report and `eval compare` show them ("the agent never used the server" when none), compare notes
200
+ when the arms' servers differ, and `report.json` records the server and the command. A server
201
+ that does not start, or a failed prepare command, stops the eval at the first run. The Claude
202
+ Code baseline refuses both options.
203
+ - The prepare command now runs before the agent's runtime starts, so a note that it writes into
204
+ `GARUDA.md` reaches the agent (the user's first Graphify run: the agent never called the server,
205
+ and a note written later would not have been read). A `~/.garuda/mcp.json` whose `"servers"` is a
206
+ list now gets a message that shows the right form, not "expected record, received array".
207
+ - Docs: Graphify as an example MCP server in the tools guide (install, `mcp.json`, the note in
208
+ `GARUDA.md`, and what we measured: ×1.28 cost per accepted change on the hard suite, no change in
209
+ passes). Garuda does not include it. The T11d design gets the user's keep rule: an edit with the
210
+ same passes stays when its cost per accepted change is clearly lower.
211
+ - Tests: `waitUntil` waits up to 20 s by default (it was 5 s), under the 30 s test limit. The
212
+ user's `pnpm check` failed the M5 Ctrl-C test on a busy machine. Two test files had their own
213
+ copy with 4 or 5 s (a second M5 Ctrl-C test then failed the same way); they use `waitUntil`
214
+ now, and a test refuses a new copy.
215
+
216
+ - The proposer loop (T11d): `garuda eval tune --addendum <file>` improves a prompt addendum. A
217
+ proposer model (default `claude-opus-5-5`) reads the dev runs that failed, or each task's cost
218
+ when all passed, and writes a new version; it stays when no task goes from all runs to none and
219
+ it passes 2 more runs (`--margin`) or as many at a clearly lower cost per accepted change (the
220
+ user's rule). Texts that ask to skip or edit tests or checks, or to leave the sandbox, are
221
+ refused before a run; the proposer never sees the held-out tasks; `--max-cost` (default $20)
222
+ stops before a round that would pass it. At the end the held-out set compares no addendum with
223
+ the final one. Garuda never edits the file: the versions, inputs, reports and history go to
224
+ `.garuda/evals/tune-<id>/`. `garuda eval --addendum <file>` runs any suite with an addendum.
225
+ - `eval tune` after the user's first run (Haiku 5.5 agent, Opus 5.5 proposer, $0.65): the kept
226
+ version cut the held-out cost per accepted change to ×0.81 (×0.75 to ×0.88), all runs passing.
227
+ The run also found four faults, now fixed: two of three proposals were refused for phrases in
228
+ rules against cheating ("never modify tests to make them pass") or about reading ("skip tests
229
+ you won't run"), so a negation now clears a phrase and a refused text gets one more try in the
230
+ round; the held-out comparison said the reports had no task hashes; runs went one at a time
231
+ (about 16 minutes a score), so `--parallel` (default 4) runs them together; and nothing printed
232
+ during a score, so each run prints a line. Two runs of one task at the same time can no longer
233
+ take the same session file name (also for `garuda eval --parallel`).
234
+
235
+ - The system prompt tells the agent how to work in few steps: batch tool calls that do not depend
236
+ on each other, find before reading, no exploration with bash, all edits in one step, one narrow
237
+ test run, fix the code not the tests, stop when done. The rules come from the first
238
+ `garuda eval tune` run: on the held-out set, with every run passing, they cut the cost per
239
+ accepted change to ×0.81 (95% interval ×0.75 to ×0.88) with Haiku 5.5 and ×0.87 (×0.79 to
240
+ ×0.95) with Sonnet 5.5. The tuned text's rules against `remember` and for one-sentence answers
241
+ are left out: they served the evals, not users. The prompt's own wording, measured against
242
+ 0.17.0's prompt on the held-out set (3 runs a task, seed 7): with Sonnet 5.5, 30/30 both and
243
+ ×0.90 (×0.84 to ×0.96); with Haiku 5.5, 30/30 and 29/30 (one miss of the invoice refund trap)
244
+ and ×0.88 (×0.79 to ×1.02), so the Haiku saving is likely but not shown with confidence.
245
+
3
246
  ## 0.17.0 (2026-10-27)
4
247
 
5
248
  - macOS: no command process outlives the command (IPI-04). Seatbelt has no process namespace, so a
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.17.0. Release notes: [CHANGELOG.md](CHANGELOG.md).
10
+ 0.18.1. Release notes: [CHANGELOG.md](CHANGELOG.md).
11
11
 
12
12
  ## What Garuda does on your machine
13
13
 
@@ -15,7 +15,8 @@ decisions. It works with Claude and with OpenAI-compatible models, local ones to
15
15
  - `write_file` and `edit_file` show a diff first. You allow once, allow for this session, or deny.
16
16
  - Commands (`bash`) run in an OS sandbox when your machine has one: Seatbelt on macOS, bubblewrap
17
17
  on Linux. There they run with no question, but they can write only in the project, temp folders
18
- and package caches, cannot read secrets in your home folder (such as `~/.ssh`), and have no
18
+ and package caches, cannot read secrets in your home folder (such as `~/.ssh`) or your personal folders (Documents,
19
+ Downloads, Pictures and the like, outside the project), and have no
19
20
  network unless you open an allowlist. So a command can change project files without a question;
20
21
  `/undo` restores the files in its snapshot (ignored files, such as build output, are not in it).
21
22
  - A command outside the sandbox asks first, unless one of your allow rules covers it. The strict
package/SECURITY.md CHANGED
@@ -6,7 +6,7 @@ Garuda runs commands and edits files for a model, so we take security reports se
6
6
 
7
7
  | Version | Supported |
8
8
  | --- | --- |
9
- | 0.17.x | Yes |
9
+ | 0.18.x | Yes |
10
10
  | Older | No |
11
11
 
12
12
  ## Node and dependencies
@@ -2,34 +2,37 @@
2
2
  import {
3
3
  Runtime,
4
4
  auditDirFor
5
- } from "./chunk-2DYMJMRQ.js";
5
+ } from "./chunk-5UDYQ5MK.js";
6
6
  import "./chunk-G2AX7MGS.js";
7
- import "./chunk-A2QAIRQL.js";
8
- import {
9
- FileSessionStore
10
- } from "./chunk-735BKABA.js";
7
+ import "./chunk-ULAJPMY7.js";
8
+ import "./chunk-JMH5W5DG.js";
11
9
  import {
12
10
  chooseModel,
13
11
  keepProviderKeys,
14
12
  loadModelsConfig,
15
13
  providerKeySource,
16
14
  resolveModel
17
- } from "./chunk-JRVWH27M.js";
18
- import "./chunk-U65DQZDB.js";
19
- import "./chunk-J4346IDH.js";
15
+ } from "./chunk-QLBCM2YQ.js";
16
+ import "./chunk-NH72AELP.js";
17
+ import "./chunk-4QRTTRVK.js";
18
+ import "./chunk-A2QAIRQL.js";
19
+ import {
20
+ FileSessionStore
21
+ } from "./chunk-NLS7GMF7.js";
22
+ import "./chunk-7KBOIPUT.js";
20
23
  import {
21
24
  loadSearchConfig
22
25
  } from "./chunk-B25VL4RU.js";
23
- import "./chunk-23FXBNBB.js";
26
+ import "./chunk-MNE7HHX2.js";
24
27
  import {
25
28
  VERSION
26
- } from "./chunk-BSK5T2ZG.js";
29
+ } from "./chunk-IYGJWX65.js";
27
30
  import "./chunk-5DEWP3YX.js";
28
- import "./chunk-ZAE25JM2.js";
29
- import "./chunk-ATW7BNIG.js";
30
31
  import "./chunk-PZS3V6P3.js";
32
+ import "./chunk-QANRZW7W.js";
33
+ import "./chunk-ATW7BNIG.js";
31
34
  import "./chunk-AOGFYUKG.js";
32
- import "./chunk-B4SJY7KT.js";
35
+ import "./chunk-LMKGSYJC.js";
33
36
  import "./chunk-BHZ6G65V.js";
34
37
  import "./chunk-P6XWWIEK.js";
35
38
  import "./chunk-UIRJOQQS.js";
@@ -47,12 +50,12 @@ async function acpCommand(options) {
47
50
  `);
48
51
  return 1;
49
52
  }
50
- const { setupCommand, terminalIO } = await import("./setupCommand-GBQIVYJC.js");
53
+ const { setupCommand, terminalIO } = await import("./setupCommand-OSOMFHDQ.js");
51
54
  return setupCommand({}, { io: await terminalIO() });
52
55
  }
53
56
  const output = protocolOutput(process.stdout, process.stderr);
54
57
  const input = Readable.toWeb(process.stdin);
55
- const { acpServer, setupNeeded } = await import("./server-VEYIJHEU.js");
58
+ const { acpServer, setupNeeded } = await import("./server-ASM73QVY.js");
56
59
  const server = acpServer({
57
60
  version: VERSION,
58
61
  // Not on native Windows: the setup there could not keep a key private (no 0600).
@@ -190,4 +193,4 @@ export {
190
193
  prepare,
191
194
  protocolOutput
192
195
  };
193
- //# sourceMappingURL=acpCommand-7EY3NHPG.js.map
196
+ //# sourceMappingURL=acpCommand-JAYNMIMP.js.map
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ compareReports,
4
+ costPerPass,
5
+ formatComparison,
6
+ loadReport,
7
+ repoEvalTasks
8
+ } from "./chunk-CWVXOSAA.js";
9
+ import {
10
+ escapeHtml
11
+ } from "./chunk-TLA2PDGM.js";
12
+ import {
13
+ loadModelsConfig,
14
+ resolveModel
15
+ } from "./chunk-QLBCM2YQ.js";
16
+ import "./chunk-NH72AELP.js";
17
+ import "./chunk-DQSCCE77.js";
18
+ import "./chunk-4QRTTRVK.js";
19
+ import "./chunk-A2QAIRQL.js";
20
+ import "./chunk-2OYTNNUJ.js";
21
+ import {
22
+ newSessionId
23
+ } from "./chunk-NLS7GMF7.js";
24
+ import "./chunk-7KBOIPUT.js";
25
+ import "./chunk-ATW7BNIG.js";
26
+ import "./chunk-WK5NE6RY.js";
27
+ import "./chunk-AOGFYUKG.js";
28
+ import "./chunk-QXTHEFKP.js";
29
+ import "./chunk-LMKGSYJC.js";
30
+ import "./chunk-BHZ6G65V.js";
31
+ import "./chunk-P6XWWIEK.js";
32
+ import "./chunk-UIRJOQQS.js";
33
+ import "./chunk-4LFJFDCF.js";
34
+ import "./chunk-3NJNVZTK.js";
35
+
36
+ // src/cli/advise.ts
37
+ import { mkdirSync, writeFileSync } from "fs";
38
+ import { join } from "path";
39
+ import { createInterface } from "readline/promises";
40
+ var ADVISE_MODELS = ["claude-haiku-5-5", "claude-sonnet-5-5"];
41
+ var ADVISE_REPEAT = 2;
42
+ var ADVISE_MAX_COST = 2;
43
+ var GUESS_INPUT_TOKENS = 6e4;
44
+ var GUESS_OUTPUT_TOKENS = 1500;
45
+ function estimateUsd(price, runs) {
46
+ if (price === void 0) return void 0;
47
+ return runs * (GUESS_INPUT_TOKENS * price.input + GUESS_OUTPUT_TOKENS * price.output) / 1e6;
48
+ }
49
+ function adviceText(a, b, c, tasks) {
50
+ const rough = tasks < 10 ? ` With ${tasks} task${tasks === 1 ? "" : "s"}, this is a rough guide.` : "";
51
+ if (c.verdict === "B passes more")
52
+ return `Advice: ${b} for this repo. It passes more often.${rough}`;
53
+ if (c.verdict === "B passes less")
54
+ return `Advice: ${a} for this repo. It passes more often.${rough}`;
55
+ const ratio = c.costRatio;
56
+ if (ratio === void 0) {
57
+ return `Advice: no clear difference in passes, and the cost could not be compared.${rough}`;
58
+ }
59
+ const times = (x) => `${x.toFixed(1)} times`;
60
+ if (ratio.verdict === "B costs less per accepted change") {
61
+ return `Advice: ${b} for this repo. It passes as often, and ${a} costs ${times(1 / ratio.value)} as much per accepted change.${rough}`;
62
+ }
63
+ if (ratio.verdict === "B costs more per accepted change") {
64
+ return `Advice: ${a} for this repo. It passes as often, and ${b} costs ${times(ratio.value)} as much per accepted change.${rough}`;
65
+ }
66
+ const cheaper = ratio.value <= 1 ? b : a;
67
+ return `Advice: either. No clear difference in passes or in cost; ${cheaper} was cheaper in these runs.${rough}`;
68
+ }
69
+ function adviceHtml(comparison, advice) {
70
+ return [
71
+ "<!doctype html>",
72
+ '<html lang="en"><head><meta charset="utf-8">',
73
+ `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">`,
74
+ "<title>Garuda advise</title>",
75
+ "<style>body{font:15px/1.5 system-ui,sans-serif;max-width:900px;margin:32px auto;padding:0 16px}",
76
+ "pre{font:13px/1.45 ui-monospace,monospace;white-space:pre-wrap}.advice{font-weight:600}</style>",
77
+ "</head><body><h1>Which model for this repository</h1>",
78
+ `<p class="advice">${escapeHtml(advice)}</p>`,
79
+ `<pre>${escapeHtml(comparison)}</pre>`,
80
+ "</body></html>",
81
+ ""
82
+ ].join("\n");
83
+ }
84
+ async function runAdvise(options) {
85
+ const [a, b] = options.models ?? ADVISE_MODELS;
86
+ if (a === void 0 || b === void 0 || options.models?.length === 1 || (options.models?.length ?? 2) > 2) {
87
+ process.stderr.write("Give two models: --models <a>,<b>.\n");
88
+ return 1;
89
+ }
90
+ const repeat = options.repeat ?? ADVISE_REPEAT;
91
+ const maxCost = options.maxCost ?? ADVISE_MAX_COST;
92
+ const seed = options.seed ?? 1;
93
+ if (!Number.isInteger(repeat) || repeat < 1 || !(maxCost > 0)) {
94
+ process.stderr.write("--repeat must be a whole number of 1 or more, and --max-cost above 0.\n");
95
+ return 1;
96
+ }
97
+ const { repoSuite, runEvalCommand } = await import("./evalCommand-BLJJ4G76.js");
98
+ const suite = await repoSuite();
99
+ if (typeof suite === "string") {
100
+ process.stderr.write(`${suite}
101
+ `);
102
+ return 1;
103
+ }
104
+ const tasks = repoEvalTasks(suite).length;
105
+ const models = await loadModelsConfig();
106
+ if (models.problem !== void 0) {
107
+ process.stderr.write(`${models.problem}
108
+ `);
109
+ return 1;
110
+ }
111
+ let prices;
112
+ try {
113
+ prices = [resolveModel(a, models.config).info.price, resolveModel(b, models.config).info.price];
114
+ } catch (error) {
115
+ process.stderr.write(`${error.message}
116
+ `);
117
+ return 1;
118
+ }
119
+ const runs = tasks * repeat;
120
+ const estimates = prices.map((p) => estimateUsd(p, runs));
121
+ const total = estimates[0] === void 0 || estimates[1] === void 0 ? void 0 : estimates[0] + estimates[1];
122
+ process.stderr.write(
123
+ [
124
+ `Your repo: ${tasks} task${tasks === 1 ? "" : "s"} from its history (tests that fail before the commit and pass after it).`,
125
+ `Models: ${a}, ${b} \xB7 ${repeat} run${repeat === 1 ? "" : "s"} a task \xB7 ${2 * runs} runs, ${total === void 0 ? "cost unknown" : `about $${total.toFixed(2)} (a guess)`}.`,
126
+ `Cap: --max-cost $${maxCost}; it stops before a model that would pass it.`,
127
+ ""
128
+ ].join("\n")
129
+ );
130
+ if (total !== void 0 && total > maxCost) {
131
+ process.stderr.write(`The guess passes the cap: raise --max-cost, or lower --repeat.
132
+ `);
133
+ return 1;
134
+ }
135
+ if (options.yes !== true) {
136
+ if (!process.stdin.isTTY) {
137
+ process.stderr.write("Add --yes to start without a question.\n");
138
+ return 1;
139
+ }
140
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
141
+ const answer = await rl.question("Start? (y/N) ");
142
+ rl.close();
143
+ if (!/^y(es)?$/i.test(answer.trim())) return 1;
144
+ }
145
+ const dir = join(process.cwd(), ".garuda", "evals", `advise-${newSessionId()}`);
146
+ mkdirSync(dir, { recursive: true });
147
+ const run = (model, label) => runEvalCommand({ model, suite: "repo", repeat, shuffle: true, seed, outDir: join(dir, label) });
148
+ await run(a, "a");
149
+ const first = await loadReport(join(dir, "a"));
150
+ const spent = costPerPass(first.results);
151
+ const spentA = spent.kind === "unknown" ? void 0 : spent.totalUsd;
152
+ const ratio = prices[0] === void 0 || prices[1] === void 0 ? void 0 : prices[1].input / prices[0].input;
153
+ if (spentA !== void 0 && ratio !== void 0 && spentA * (1 + ratio) > maxCost) {
154
+ process.stderr.write(
155
+ `Stopped before ${b}: ${a} cost $${spentA.toFixed(2)}, so ${b} would take the total past $${maxCost}.
156
+ `
157
+ );
158
+ return 1;
159
+ }
160
+ await run(b, "b");
161
+ const second = await loadReport(join(dir, "b"));
162
+ const result = compareReports(first, second, { seed });
163
+ if ("problems" in result) {
164
+ process.stderr.write(`The two runs cannot be compared:
165
+ ${result.problems.join("\n")}
166
+ `);
167
+ return 1;
168
+ }
169
+ const comparison = formatComparison(first, second, result.comparison);
170
+ const advice = adviceText(a, b, result.comparison, tasks);
171
+ writeFileSync(join(dir, "advice.txt"), `${advice}
172
+
173
+ ${comparison}
174
+ `);
175
+ writeFileSync(join(dir, "report.html"), adviceHtml(comparison, advice));
176
+ process.stdout.write(`
177
+ ${comparison}
178
+
179
+ ${advice}
180
+ Report: ${join(dir, "report.html")}
181
+ `);
182
+ return 0;
183
+ }
184
+ export {
185
+ ADVISE_MAX_COST,
186
+ ADVISE_MODELS,
187
+ ADVISE_REPEAT,
188
+ GUESS_INPUT_TOKENS,
189
+ GUESS_OUTPUT_TOKENS,
190
+ adviceHtml,
191
+ adviceText,
192
+ estimateUsd,
193
+ runAdvise
194
+ };
195
+ //# sourceMappingURL=advise-HDKJGNBL.js.map
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseTestCount
4
+ } from "./chunk-4QRTTRVK.js";
5
+ import {
6
+ formatCost,
7
+ jobCost,
8
+ sumCosts
9
+ } from "./chunk-3MRQJKUO.js";
10
+ import "./chunk-AOGFYUKG.js";
11
+ import {
12
+ JOBS_DIR,
13
+ listJobs
14
+ } from "./chunk-VTMD5IHD.js";
15
+ import "./chunk-UIRJOQQS.js";
16
+ import "./chunk-4LFJFDCF.js";
17
+ import "./chunk-3NJNVZTK.js";
18
+
19
+ // src/jobs/brief.ts
20
+ import { mkdir, readFile, writeFile } from "fs/promises";
21
+ import { join } from "path";
22
+ var BRIEF_FILE = "brief.seen";
23
+ var FIRST_BRIEF_MS = 24 * 36e5;
24
+ function testsText(job) {
25
+ const proof = job.result?.proof;
26
+ const after = proof?.after;
27
+ if (after === void 0) return "no tests";
28
+ const count = (tail) => tail === void 0 ? void 0 : parseTestCount(tail)?.total;
29
+ const before = count(proof?.before?.tail);
30
+ const now = count(after.tail);
31
+ const passed = after.exitCode === 0 && !after.timedOut;
32
+ if (before !== void 0 && now !== void 0 && passed) return `tests ${before} \u2192 ${now}`;
33
+ return passed ? "tests pass" : "tests fail after the change";
34
+ }
35
+ function jobLine(job, paint) {
36
+ const verdict = job.result?.proof?.verdict;
37
+ const ok = job.status === "done" && verdict === "ready";
38
+ const mark = ok ? paint("green", "\u2713") : paint("red", "\u2717");
39
+ const what = verdict === "ready" ? "ready to merge" : verdict === "needs-look" ? "needs a look" : job.status === "done" ? "done, no verdict" : job.status;
40
+ const title = job.title.length > 32 ? `${job.title.slice(0, 31)}\u2026` : job.title;
41
+ return ` ${mark} ${title.padEnd(32)} ${what.padEnd(16)} ${testsText(job).padEnd(26)} ${paint("dim", job.branch)}`;
42
+ }
43
+ function briefText(jobs, paint) {
44
+ if (jobs.length === 0) return void 0;
45
+ const cost = sumCosts(jobs.flatMap((j) => j.result === void 0 ? [] : [jobCost(j.result)]));
46
+ return [
47
+ paint(
48
+ "bold",
49
+ `While you were away: ${jobs.length} job${jobs.length === 1 ? "" : "s"} ran (${formatCost(cost)})`
50
+ ),
51
+ ...jobs.map((j) => jobLine(j, paint)),
52
+ paint(
53
+ "dim",
54
+ " /jobs <id> shows a job's report and review \xB7 git diff <base>...<branch> shows its change"
55
+ )
56
+ ].join("\n");
57
+ }
58
+ function endedSince(jobs, since) {
59
+ return jobs.filter((j) => j.endedAt !== void 0 && Date.parse(j.endedAt) > since).filter((j) => j.status === "done" || j.status === "stopped" || j.status === "failed").sort((a, b) => Date.parse(a.endedAt ?? "") - Date.parse(b.endedAt ?? ""));
60
+ }
61
+ async function morningBrief(root, paint, now = Date.now()) {
62
+ const dir = join(root, JOBS_DIR);
63
+ const file = join(dir, BRIEF_FILE);
64
+ let since = now - FIRST_BRIEF_MS;
65
+ try {
66
+ const data = JSON.parse(await readFile(file, "utf8"));
67
+ if (typeof data.seenAt === "string" && !Number.isNaN(Date.parse(data.seenAt))) {
68
+ since = Date.parse(data.seenAt);
69
+ }
70
+ } catch {
71
+ }
72
+ const jobs = endedSince(await listJobs(root), since);
73
+ if (jobs.length === 0) return void 0;
74
+ try {
75
+ await mkdir(dir, { recursive: true });
76
+ await writeFile(file, `${JSON.stringify({ seenAt: new Date(now).toISOString() })}
77
+ `);
78
+ } catch {
79
+ }
80
+ return briefText(jobs, paint);
81
+ }
82
+ export {
83
+ BRIEF_FILE,
84
+ FIRST_BRIEF_MS,
85
+ briefText,
86
+ endedSince,
87
+ jobLine,
88
+ morningBrief,
89
+ testsText
90
+ };
91
+ //# sourceMappingURL=brief-RWKESOPT.js.map
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CHAT_STALE,
4
+ garudaCommit,
5
+ staleBuildNote
6
+ } from "./chunk-YAQ6QLKB.js";
7
+ import "./chunk-QXTHEFKP.js";
8
+ import "./chunk-LMKGSYJC.js";
9
+ import "./chunk-BHZ6G65V.js";
10
+ import "./chunk-P6XWWIEK.js";
11
+ import "./chunk-UIRJOQQS.js";
12
+ import "./chunk-4LFJFDCF.js";
13
+ import "./chunk-3NJNVZTK.js";
14
+ export {
15
+ CHAT_STALE,
16
+ garudaCommit,
17
+ staleBuildNote
18
+ };
19
+ //# sourceMappingURL=buildCheck-HB46NWLX.js.map