@garuda-agent/garuda 0.16.1 → 0.17.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +56 -1074
  3. package/SECURITY.md +9 -2
  4. package/dist/{acpCommand-OTAYW7BS.js → acpCommand-7EY3NHPG.js} +18 -16
  5. package/dist/{anthropic-NQDBCHJU.js → anthropic-NQIZLD64.js} +12 -6
  6. package/dist/{chunk-TILFPYWO.js → chunk-2DYMJMRQ.js} +1880 -982
  7. package/dist/{chunk-YQTGMCU6.js → chunk-3MRQJKUO.js} +29 -5
  8. package/dist/{chunk-WMOLXTBU.js → chunk-4LFJFDCF.js} +2 -1
  9. package/dist/{chunk-44QN5SWZ.js → chunk-55CFEEIN.js} +22 -2
  10. package/dist/{chunk-USCUXCAU.js → chunk-5B5HDTUR.js} +3 -3
  11. package/dist/{chunk-TUUEBHG2.js → chunk-6XHON3Q6.js} +2 -2
  12. package/dist/chunk-735BKABA.js +472 -0
  13. package/dist/{chunk-B6FA4I7Q.js → chunk-7TH3NHCQ.js} +2 -2
  14. package/dist/{chunk-OUCV3FCT.js → chunk-AOGFYUKG.js} +3 -3
  15. package/dist/{chunk-GXZACJUI.js → chunk-B4SJY7KT.js} +322 -39
  16. package/dist/{chunk-3EQU2GUX.js → chunk-BHZ6G65V.js} +5 -1
  17. package/dist/chunk-BSK5T2ZG.js +9 -0
  18. package/dist/chunk-J4346IDH.js +156 -0
  19. package/dist/{chunk-NP6RIC25.js → chunk-JRVWH27M.js} +7 -7
  20. package/dist/{chunk-HUIK3WXO.js → chunk-P6XWWIEK.js} +20 -3
  21. package/dist/{chunk-HE2K657U.js → chunk-TJIWD7R7.js} +47 -16
  22. package/dist/chunk-U65DQZDB.js +143 -0
  23. package/dist/chunk-UIRJOQQS.js +118 -0
  24. package/dist/{chunk-KKB5W47S.js → chunk-VTMD5IHD.js} +26 -2
  25. package/dist/{chunk-BQTUOO4G.js → chunk-Z66BWTMD.js} +81 -32
  26. package/dist/cli/index.js +99 -41
  27. package/dist/{create-VXIH2GUZ.js → create-W6BV75QX.js} +18 -12
  28. package/dist/{deadline-F5RJJ2QW.js → deadline-TP7ZGLT2.js} +4 -2
  29. package/dist/{evalCommand-RGMZRYDR.js → evalCommand-YVQAWUZE.js} +50 -29
  30. package/dist/{export-HN3CT7AV.js → export-SJ3DUP6W.js} +2 -2
  31. package/dist/{inkChat-WEDGQMCY.js → inkChat-FNUBK43U.js} +14 -13
  32. package/dist/install-TKWN5AYH.js +19 -0
  33. package/dist/{job-3NYDMP2M.js → job-7JOI4YIQ.js} +6 -2
  34. package/dist/jobCommand-FE6XN3YD.js +33 -0
  35. package/dist/{launchd-SSX362VG.js → launchd-3A3ONEW5.js} +9 -8
  36. package/dist/{lspCommand-JRXKRSL7.js → lspCommand-ZZ6R4PA2.js} +7 -6
  37. package/dist/{manager-773PUDFE.js → manager-73MPYFTP.js} +7 -6
  38. package/dist/{nightCommand-65MUEV32.js → nightCommand-OODJNWDT.js} +26 -20
  39. package/dist/{openaiCompatible-CJWVJP5M.js → openaiCompatible-6TLDDW5U.js} +29 -15
  40. package/dist/{policy-EJPLEEXP.js → policy-ZJ7RMOFN.js} +10 -4
  41. package/dist/{remove-6GDSTAIO.js → remove-42WQNVPX.js} +8 -7
  42. package/dist/{server-NGKVIQEV.js → server-VEYIJHEU.js} +2 -2
  43. package/dist/{setupCommand-VA7D5MZK.js → setupCommand-GBQIVYJC.js} +45 -13
  44. package/dist/text-LS35WSRL.js +17 -0
  45. package/native/bin/garuda-reap +0 -0
  46. package/native/garuda-reap.c +113 -0
  47. package/package.json +39 -13
  48. package/dist/chunk-EIXUSVFX.js +0 -116
  49. package/dist/chunk-R7I2NBPX.js +0 -71
  50. package/dist/chunk-ULAJPMY7.js +0 -107
  51. package/dist/chunk-VBZCNMUD.js +0 -9
  52. package/dist/install-EWDSPFFZ.js +0 -18
  53. package/dist/jobCommand-ACSGSBQ4.js +0 -32
  54. package/dist/text-J2GD647J.js +0 -11
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  passed,
4
4
  testSummary
5
- } from "./chunk-OUCV3FCT.js";
5
+ } from "./chunk-AOGFYUKG.js";
6
6
 
7
7
  // src/jobs/text.ts
8
8
  function jobPrompt(request, plan) {
@@ -22,7 +22,28 @@ function jobPrompt(request, plan) {
22
22
  "</plan>"
23
23
  ].join("\n");
24
24
  }
25
- var cost = (usd) => usd === void 0 ? "unknown" : `$${usd.toFixed(4)}`;
25
+ var cost = (usd, incomplete = false) => formatCost({ usd, incomplete });
26
+ function formatCost(c) {
27
+ if (c.usd === void 0) return "unknown";
28
+ return c.incomplete ? `\u2265 $${c.usd.toFixed(4)} (incomplete)` : `$${c.usd.toFixed(4)}`;
29
+ }
30
+ function sumCosts(costs) {
31
+ let usd = 0;
32
+ let incomplete = false;
33
+ for (const c of costs) {
34
+ usd = usd === void 0 || c.usd === void 0 ? void 0 : usd + c.usd;
35
+ incomplete ||= c.incomplete;
36
+ }
37
+ return { usd, incomplete };
38
+ }
39
+ function jobCost(r) {
40
+ const run = { usd: r.costUsd, incomplete: r.costIncomplete === true };
41
+ const proof = r.proof;
42
+ const review = proof?.review;
43
+ const reviewIncomplete = proof?.reviewCostIncomplete === true;
44
+ if (review === void 0) return sumCosts([run, { usd: 0, incomplete: reviewIncomplete }]);
45
+ return sumCosts([run, { usd: review.costUsd, incomplete: reviewIncomplete }]);
46
+ }
26
47
  function jobReport(job) {
27
48
  const r = job.result;
28
49
  const lines = [`# Garuda job ${job.id}: ${job.title}`, ""];
@@ -40,7 +61,7 @@ function jobReport(job) {
40
61
  if (job.startedAt !== void 0) lines.push(`- Started: ${job.startedAt}`);
41
62
  if (r === void 0) return lines.join("\n");
42
63
  lines.push(
43
- `- Steps: ${r.steps} \xB7 tokens: ${Math.round(r.tokens / 100) / 10}k \xB7 cost: ${cost(r.costUsd)} \xB7 time: ${Math.round(r.durationMs / 1e3)} s`
64
+ `- Steps: ${r.steps} \xB7 tokens: ${Math.round(r.tokens / 100) / 10}k \xB7 cost: ${cost(r.costUsd, r.costIncomplete === true)} \xB7 time: ${Math.round(r.durationMs / 1e3)} s`
44
65
  );
45
66
  if (r.sessionId !== void 0) lines.push(`- Session: ${r.sessionId}`);
46
67
  if (r.modelCalls !== void 0) {
@@ -96,7 +117,7 @@ function proofLines(job, proof) {
96
117
  if (proof.flags.length === 0) out.push("None.");
97
118
  for (const f of proof.flags) out.push(`- ${f.level === "stop" ? "STOP" : "look"}: ${f.text}`);
98
119
  if (proof.review !== void 0) {
99
- const cost2 = proof.review.costUsd === void 0 ? "" : ` (${`$${proof.review.costUsd.toFixed(4)}`})`;
120
+ const cost2 = proof.review.costUsd === void 0 ? "" : ` (${formatCost({ usd: proof.review.costUsd, incomplete: proof.reviewCostIncomplete === true })})`;
100
121
  out.push("", `## Review by a principal engineer${cost2}`, "", proof.review.text);
101
122
  } else if (proof.reviewError !== void 0) {
102
123
  out.push("", "## Review", "", `The review failed: ${proof.reviewError}`);
@@ -106,6 +127,9 @@ function proofLines(job, proof) {
106
127
 
107
128
  export {
108
129
  jobPrompt,
130
+ formatCost,
131
+ sumCosts,
132
+ jobCost,
109
133
  jobReport
110
134
  };
111
- //# sourceMappingURL=chunk-YQTGMCU6.js.map
135
+ //# sourceMappingURL=chunk-3MRQJKUO.js.map
@@ -56,6 +56,7 @@ export {
56
56
  isInside,
57
57
  resolveInRoot,
58
58
  realRelative,
59
+ realpathOfNearestExisting,
59
60
  displayPath
60
61
  };
61
- //# sourceMappingURL=chunk-WMOLXTBU.js.map
62
+ //# sourceMappingURL=chunk-4LFJFDCF.js.map
@@ -2,7 +2,10 @@
2
2
  import {
3
3
  JobGitError,
4
4
  git
5
- } from "./chunk-TUUEBHG2.js";
5
+ } from "./chunk-6XHON3Q6.js";
6
+ import {
7
+ inWriteScope
8
+ } from "./chunk-UIRJOQQS.js";
6
9
 
7
10
  // src/jobs/worktree.ts
8
11
  import { createHash } from "crypto";
@@ -86,6 +89,22 @@ async function commitJob(executor, job, message) {
86
89
  await git(executor, job.worktree, ["commit", "--no-verify", "-q", "-m", message]);
87
90
  return (await git(executor, job.worktree, ["rev-parse", "HEAD"])).stdout.trim();
88
91
  }
92
+ async function outsideScope(executor, job) {
93
+ if (job.writeScope === void 0) return [];
94
+ const exclude = [...NEVER_COMMIT, ...job.links].map((p) => `:(exclude)${p}`);
95
+ const status = await git(executor, job.worktree, [
96
+ "status",
97
+ "--porcelain=v1",
98
+ "-z",
99
+ "--untracked-files=all",
100
+ "--no-renames",
101
+ "--",
102
+ ".",
103
+ ...exclude
104
+ ]);
105
+ const scope = job.writeScope;
106
+ return status.stdout.split("\0").filter((entry) => entry.length > 3).map((entry) => entry.slice(3)).filter((path) => !inWriteScope(path, scope));
107
+ }
89
108
  async function jobChanges(executor, job) {
90
109
  const range = [job.base, job.branch];
91
110
  const names = await git(executor, job.root, [
@@ -133,6 +152,7 @@ export {
133
152
  linkCandidates,
134
153
  prepareWorktree,
135
154
  commitJob,
155
+ outsideScope,
136
156
  jobChanges
137
157
  };
138
- //# sourceMappingURL=chunk-44QN5SWZ.js.map
158
+ //# sourceMappingURL=chunk-55CFEEIN.js.map
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  hostCommand,
4
4
  shellWord
5
- } from "./chunk-TUUEBHG2.js";
5
+ } from "./chunk-6XHON3Q6.js";
6
6
  import {
7
7
  JOBS_DIR
8
- } from "./chunk-KKB5W47S.js";
8
+ } from "./chunk-VTMD5IHD.js";
9
9
 
10
10
  // src/jobs/launchd.ts
11
11
  import { createHash } from "crypto";
@@ -154,4 +154,4 @@ export {
154
154
  nightAgentTime,
155
155
  removeAgent
156
156
  };
157
- //# sourceMappingURL=chunk-USCUXCAU.js.map
157
+ //# sourceMappingURL=chunk-5B5HDTUR.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DEFAULT_ENV_ALLOWLIST
4
- } from "./chunk-GXZACJUI.js";
4
+ } from "./chunk-B4SJY7KT.js";
5
5
 
6
6
  // src/jobs/git.ts
7
7
  var GIT_TIMEOUT_MS = 12e4;
@@ -46,4 +46,4 @@ export {
46
46
  git,
47
47
  hostCommand
48
48
  };
49
- //# sourceMappingURL=chunk-TUUEBHG2.js.map
49
+ //# sourceMappingURL=chunk-6XHON3Q6.js.map
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Redactor,
4
+ contextSize,
5
+ totalTokens
6
+ } from "./chunk-J4346IDH.js";
7
+ import {
8
+ cleanLine
9
+ } from "./chunk-PZS3V6P3.js";
10
+ import {
11
+ ZERO_USAGE,
12
+ addUsage,
13
+ reportedUsage
14
+ } from "./chunk-BHZ6G65V.js";
15
+ import {
16
+ formatRule
17
+ } from "./chunk-3NJNVZTK.js";
18
+
19
+ // src/session/store.ts
20
+ import { randomBytes } from "crypto";
21
+ import { appendFileSync, mkdirSync } from "fs";
22
+ import { readdir, readFile, rm, stat, utimes } from "fs/promises";
23
+ import { basename, join } from "path";
24
+ var SESSIONS_DIR = join(".garuda", "sessions");
25
+ var FileSessionStore = class {
26
+ dir;
27
+ redactor;
28
+ constructor(root, redactor = new Redactor()) {
29
+ this.dir = join(root, SESSIONS_DIR);
30
+ this.redactor = redactor;
31
+ }
32
+ path(sessionId) {
33
+ if (!SESSION_ID.test(sessionId)) throw new Error(`"${sessionId}" is not a session id.`);
34
+ return join(this.dir, `${sessionId}.jsonl`);
35
+ }
36
+ open(sessionId) {
37
+ return this.journal(this.dir, this.path(sessionId));
38
+ }
39
+ /** `.garuda/sessions/<session id>/<child id>.jsonl`. */
40
+ childPath(sessionId, childId) {
41
+ return join(this.dir, safeId(sessionId), `${safeId(childId)}.jsonl`);
42
+ }
43
+ openChild(sessionId, childId) {
44
+ return this.journal(join(this.dir, safeId(sessionId)), this.childPath(sessionId, childId));
45
+ }
46
+ journal(dir, file) {
47
+ mkdirSync(dir, { recursive: true, mode: 448 });
48
+ return {
49
+ write: (record) => {
50
+ const line = { t: (/* @__PURE__ */ new Date()).toISOString(), ...record };
51
+ appendFileSync(file, `${JSON.stringify(this.redactor.value(line))}
52
+ `, { mode: 384 });
53
+ }
54
+ };
55
+ }
56
+ async read(sessionId) {
57
+ return parseRecords(await readFile(this.path(sessionId), "utf8"));
58
+ }
59
+ async setTitle(sessionId, title) {
60
+ const file = this.path(sessionId);
61
+ const before = await stat(file);
62
+ this.open(sessionId).write({ type: "title", title });
63
+ await utimes(file, before.atime, before.mtime);
64
+ }
65
+ async remove(sessionId) {
66
+ await rm(this.path(sessionId));
67
+ await rm(join(this.dir, safeId(sessionId)), { recursive: true, force: true });
68
+ }
69
+ async latest() {
70
+ return (await this.list())[0]?.id;
71
+ }
72
+ async list() {
73
+ let names;
74
+ try {
75
+ names = (await readdir(this.dir)).filter((name) => name.endsWith(".jsonl"));
76
+ } catch {
77
+ return [];
78
+ }
79
+ const out = [];
80
+ for (const name of names) {
81
+ const info = await stat(join(this.dir, name)).catch(() => void 0);
82
+ if (info?.isFile()) out.push({ id: basename(name, ".jsonl"), updated: info.mtime });
83
+ }
84
+ return out.sort((a, b) => b.updated.getTime() - a.updated.getTime());
85
+ }
86
+ };
87
+ var SESSION_ID = /^[A-Za-z0-9_-]{1,80}$/;
88
+ function safeId(id) {
89
+ return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80) || "_";
90
+ }
91
+ function parseRecords(text) {
92
+ const lines = text.split("\n").filter((line) => line.trim() !== "");
93
+ const records = [];
94
+ lines.forEach((line, i) => {
95
+ try {
96
+ records.push(JSON.parse(line));
97
+ } catch (error) {
98
+ if (i < lines.length - 1) throw new Error(`Session file line ${i + 1} is not valid JSON.`);
99
+ void error;
100
+ }
101
+ });
102
+ return records;
103
+ }
104
+ function newSessionId(now = /* @__PURE__ */ new Date()) {
105
+ const pad = (n) => String(n).padStart(2, "0");
106
+ const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
107
+ const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
108
+ return `${date}-${time}-${randomBytes(2).toString("hex")}`;
109
+ }
110
+
111
+ // src/session/fileTracker.ts
112
+ import { createHash } from "crypto";
113
+ var FileTracker = class {
114
+ seen = /* @__PURE__ */ new Map();
115
+ /** "path|offset|limit" → hash of the whole file at that read. */
116
+ reads = /* @__PURE__ */ new Map();
117
+ /** Record the content the agent now knows for `path` (absolute). */
118
+ record(path, content) {
119
+ this.seen.set(path, hash(content));
120
+ }
121
+ /** "unread", "changed" or "current". */
122
+ status(path, content) {
123
+ const known = this.seen.get(path);
124
+ if (known === void 0) return "unread";
125
+ return known === hash(content) ? "current" : "changed";
126
+ }
127
+ /**
128
+ * Record a read of some lines. Returns true when the agent already read the same lines
129
+ * of the same content, so the output is still in its context.
130
+ */
131
+ noteRead(path, content, offset, limit) {
132
+ const key = `${path}|${offset}|${limit}`;
133
+ const digest = hash(content);
134
+ const repeat = this.reads.get(key) === digest;
135
+ this.reads.set(key, digest);
136
+ return repeat;
137
+ }
138
+ /**
139
+ * Compaction cuts or summarises old tool outputs, so earlier reads may no longer be in the context.
140
+ * The edit freshness records stay: they are about the file, not the context.
141
+ */
142
+ forgetReads() {
143
+ this.reads.clear();
144
+ }
145
+ };
146
+ function hash(content) {
147
+ return createHash("sha256").update(content).digest("hex");
148
+ }
149
+
150
+ // src/session/undo.ts
151
+ var MAX_UNDO_POINTS = 50;
152
+ var PROMPT_CHARS = 80;
153
+ function emptyUndoState() {
154
+ return { points: [], redo: [] };
155
+ }
156
+ function pushPoint(state, tree, messages, prompt) {
157
+ const line = prompt.replace(/\s+/g, " ").trim();
158
+ state.points.push({
159
+ tree,
160
+ messages,
161
+ conversation: true,
162
+ prompt: line.length > PROMPT_CHARS ? `${line.slice(0, PROMPT_CHARS)}\u2026` : line
163
+ });
164
+ if (state.points.length > MAX_UNDO_POINTS) state.points.shift();
165
+ state.redo = [];
166
+ }
167
+ function popPoint(state, messages, after) {
168
+ const point = state.points.pop();
169
+ if (point === void 0) return void 0;
170
+ const removed = point.conversation ? messages.splice(point.messages) : [];
171
+ state.redo.push({ point, after, removed });
172
+ return point;
173
+ }
174
+ function redoPoint(state, messages) {
175
+ const entry = state.redo.pop();
176
+ if (entry === void 0) return void 0;
177
+ messages.push(...entry.removed);
178
+ state.points.push(entry.point);
179
+ return entry;
180
+ }
181
+ function compacted(state) {
182
+ for (const point of state.points) point.conversation = false;
183
+ for (const entry of state.redo) {
184
+ entry.point.conversation = false;
185
+ entry.removed = [];
186
+ }
187
+ }
188
+ function newUserMessage(state) {
189
+ state.redo = [];
190
+ }
191
+
192
+ // src/session/session.ts
193
+ function createSession(root, id = newSessionId(), journal) {
194
+ const session = {
195
+ id,
196
+ root,
197
+ messages: [],
198
+ usage: { ...ZERO_USAGE },
199
+ costUsd: 0,
200
+ costIncomplete: false,
201
+ contextTokens: 0,
202
+ unreportedTokens: 0,
203
+ childDebits: /* @__PURE__ */ new Map(),
204
+ files: new FileTracker(),
205
+ undo: emptyUndoState()
206
+ };
207
+ if (journal !== void 0) session.journal = journal;
208
+ return session;
209
+ }
210
+ function addUserMessage(session, text, notes = [], attachments = []) {
211
+ const message = {
212
+ role: "user",
213
+ content: [
214
+ { type: "text", text },
215
+ ...attachments.map((a) => ({ type: "text", text: a })),
216
+ ...notes.map((note) => ({
217
+ type: "text",
218
+ text: `<garuda_note>${note}</garuda_note>`
219
+ }))
220
+ ]
221
+ };
222
+ session.messages.push(message);
223
+ newUserMessage(session.undo);
224
+ session.journal?.write({ type: "user", message });
225
+ }
226
+ function addSnapshot(session, tree, prompt, durationMs) {
227
+ const messages = session.messages.length;
228
+ pushPoint(session.undo, tree, messages, prompt);
229
+ const point = session.undo.points.at(-1);
230
+ session.journal?.write({ type: "snapshot", tree, messages, prompt: point.prompt, durationMs });
231
+ }
232
+ function undoTurn(session, after) {
233
+ const point = popPoint(session.undo, session.messages, after);
234
+ if (point !== void 0) {
235
+ session.journal?.write({ type: "undo", after });
236
+ session.files.forgetReads();
237
+ }
238
+ return point;
239
+ }
240
+ function redoTurn(session) {
241
+ const entry = redoPoint(session.undo, session.messages);
242
+ if (entry !== void 0) {
243
+ session.journal?.write({ type: "redo" });
244
+ session.files.forgetReads();
245
+ }
246
+ return entry;
247
+ }
248
+ function addAssistantResponse(session, response, step, costUsd) {
249
+ if (response.usageReported === false) {
250
+ session.costIncomplete = true;
251
+ costUsd = void 0;
252
+ addCost(session, reportedUsage(response), session.costUsd === void 0 ? void 0 : 0);
253
+ } else addCost(session, reportedUsage(response), costUsd);
254
+ session.contextTokens = contextSize(response.usage);
255
+ if (response.content.length > 0) {
256
+ session.messages.push({ role: "assistant", content: response.content });
257
+ }
258
+ session.journal?.write({
259
+ type: "assistant",
260
+ step,
261
+ response,
262
+ ...costUsd === void 0 ? {} : { costUsd }
263
+ });
264
+ }
265
+ function addToolResults(session, results, calls, synthetic = false) {
266
+ const message = { role: "user", content: results };
267
+ session.messages.push(message);
268
+ session.journal?.write({
269
+ type: "tool_results",
270
+ message,
271
+ calls,
272
+ ...synthetic ? { synthetic } : {}
273
+ });
274
+ }
275
+ function addContinuation(session, text, maxTokens) {
276
+ const message = { role: "user", content: [{ type: "text", text }] };
277
+ session.messages.push(message);
278
+ session.journal?.write({ type: "continue", message, maxTokens });
279
+ }
280
+ function addCost(session, usage, costUsd) {
281
+ session.usage = addUsage(session.usage, usage);
282
+ session.costUsd = session.costUsd === void 0 || costUsd === void 0 ? void 0 : session.costUsd + costUsd;
283
+ }
284
+ function closeOpenToolCalls(session) {
285
+ const last = session.messages.at(-1);
286
+ if (last?.role !== "assistant") return 0;
287
+ const open = last.content.filter((b) => b.type === "tool_use");
288
+ if (open.length === 0) return 0;
289
+ session.files.forgetReads();
290
+ addToolResults(
291
+ session,
292
+ open.map((call) => ({
293
+ type: "tool_result",
294
+ toolUseId: call.id,
295
+ content: "Error: this call was interrupted. It may or may not have run. Check before you retry.",
296
+ isError: true
297
+ })),
298
+ open.map((call) => ({ toolUseId: call.id, name: call.name, durationMs: 0 })),
299
+ true
300
+ );
301
+ return open.length;
302
+ }
303
+ function addUnreported(session, tokens) {
304
+ if (tokens <= 0) return;
305
+ session.unreportedTokens += tokens;
306
+ session.costIncomplete = true;
307
+ session.journal?.write({ type: "unreported", tokens });
308
+ }
309
+ function budgetSpent(session) {
310
+ return totalTokens(session.usage) + session.unreportedTokens + pendingChildTokens(session);
311
+ }
312
+ function pendingChildTokens(session) {
313
+ let tokens = 0;
314
+ for (const value of session.childDebits.values()) tokens += value;
315
+ return tokens;
316
+ }
317
+ function isCostIncomplete(session) {
318
+ return session.costIncomplete || pendingChildTokens(session) > 0;
319
+ }
320
+ function addChildDebit(session, childSessionId, tokens) {
321
+ if (tokens <= 0) return;
322
+ session.childDebits.set(childSessionId, (session.childDebits.get(childSessionId) ?? 0) + tokens);
323
+ session.journal?.write({ type: "child_debit", childSessionId, tokens });
324
+ }
325
+ function addChildReport(session, report) {
326
+ addCost(session, report.usage, report.costUsd);
327
+ if (report.costIncomplete === true) session.costIncomplete = true;
328
+ settleChildDebits(session.childDebits, report);
329
+ }
330
+ function settleChildDebits(debits, report) {
331
+ const rest = Math.max(0, (debits.get(report.sessionId) ?? 0) - totalTokens(report.usage));
332
+ if (rest === 0) debits.delete(report.sessionId);
333
+ else debits.set(report.sessionId, rest);
334
+ }
335
+ function addSideUsage(session, purpose, response, costUsd) {
336
+ if (response.usageReported === false) return;
337
+ addCost(session, response.usage, costUsd);
338
+ session.journal?.write({
339
+ type: "side_usage",
340
+ purpose,
341
+ usage: response.usage,
342
+ ...costUsd === void 0 ? {} : { costUsd }
343
+ });
344
+ }
345
+
346
+ // src/permissions/projectSettings.ts
347
+ import { createHash as createHash2 } from "crypto";
348
+ function settingsRisk(settings) {
349
+ const risk = {
350
+ executorHost: settings.executor === "host",
351
+ allow: settings.allow.map(formatRule),
352
+ writePaths: [...settings.sandbox?.writePaths ?? []],
353
+ envAllow: [...settings.envAllow],
354
+ allowLocalhost: settings.web?.allowLocalhost === true
355
+ };
356
+ const formatterCommands = Object.entries(settings.formatters?.commands ?? {}).flatMap(
357
+ ([name, f]) => f === false ? [] : [`${name}: ${f.command.join(" ")}`]
358
+ );
359
+ if (formatterCommands.length > 0) risk.formatterCommands = formatterCommands;
360
+ const none = !risk.executorHost && risk.allow.length === 0 && risk.writePaths.length === 0 && risk.envAllow.length === 0 && !risk.allowLocalhost && risk.formatterCommands === void 0;
361
+ return none ? void 0 : risk;
362
+ }
363
+ function settingsRiskHash(risk) {
364
+ return createHash2("sha256").update(JSON.stringify(risk)).digest("hex");
365
+ }
366
+ function settingsRiskLines(risk) {
367
+ const lines = [];
368
+ if (risk.executorHost) lines.push('executor: "host" (commands run with no OS sandbox)');
369
+ if (risk.allow.length > 0) {
370
+ lines.push(`permissions.allow (no question for these): ${risk.allow.join(", ")}`);
371
+ }
372
+ if (risk.writePaths.length > 0) {
373
+ lines.push(`sandbox.writePaths (commands may write there): ${risk.writePaths.join(", ")}`);
374
+ }
375
+ if (risk.envAllow.length > 0) {
376
+ lines.push(`env.allow (commands see these variables): ${risk.envAllow.join(", ")}`);
377
+ }
378
+ if (risk.allowLocalhost) lines.push("web.allowLocalhost (web_fetch may reach this machine)");
379
+ if (risk.formatterCommands !== void 0) {
380
+ lines.push(
381
+ `formatters.commands (run after each edit): ${risk.formatterCommands.map(cleanLine).join("; ")}`
382
+ );
383
+ }
384
+ return lines;
385
+ }
386
+ function withoutRisk(settings) {
387
+ const { sandbox, web, formatters, ...rest } = settings;
388
+ const { writePaths: _writePaths, ...sandboxRest } = sandbox ?? {};
389
+ const keptCommands = Object.fromEntries(
390
+ Object.entries(formatters?.commands ?? {}).filter(([, f]) => f === false)
391
+ );
392
+ return {
393
+ ...rest,
394
+ ...formatters === void 0 ? {} : { formatters: { ...formatters, commands: keptCommands } },
395
+ executor: settings.executor === "host" ? "auto" : settings.executor,
396
+ allow: [],
397
+ envAllow: [],
398
+ ...sandbox === void 0 ? {} : { sandbox: sandboxRest },
399
+ ...web === void 0 ? {} : { web: { ...web, allowLocalhost: false } }
400
+ };
401
+ }
402
+ async function gateProjectSettings(options) {
403
+ const risk = settingsRisk(options.settings);
404
+ if (risk === void 0) return { settings: options.settings };
405
+ const hash2 = settingsRiskHash(risk);
406
+ const known = options.trust?.settingsHash(options.root);
407
+ if (known === hash2) return { settings: options.settings };
408
+ const lines = settingsRiskLines(risk);
409
+ if (options.ask !== void 0) {
410
+ const choice = await options.ask({
411
+ tool: "settings",
412
+ target: { kind: "input", json: "" },
413
+ title: "Project settings that loosen safety",
414
+ preview: [
415
+ `This project's .garuda/settings.json ${known === void 0 ? "asks for" : "changed and now asks for"}:`,
416
+ ...lines.map((line) => ` - ${line}`),
417
+ "A cloned repository can ship this file. Without your yes, Garuda uses safe defaults for these",
418
+ "parts; the rest of the file applies."
419
+ ].join("\n"),
420
+ isolation: options.isolation ?? "os",
421
+ question: "Use these project settings?",
422
+ choices: ["once", "session", "deny"],
423
+ labels: {
424
+ once: "Yes, for this run",
425
+ session: "Yes, and remember (asks again when they change)",
426
+ deny: "No, use safe defaults"
427
+ }
428
+ });
429
+ if (choice === "session") await options.trust?.setSettingsHash(options.root, hash2);
430
+ if (choice !== "deny") return { settings: options.settings };
431
+ }
432
+ return {
433
+ settings: withoutRisk(options.settings),
434
+ notice: [
435
+ `Project settings not applied (not approved): ${lines.join("; ")}.`,
436
+ "Start the chat in a terminal and answer yes to use them."
437
+ ].join(" ")
438
+ };
439
+ }
440
+
441
+ export {
442
+ FileTracker,
443
+ FileSessionStore,
444
+ parseRecords,
445
+ newSessionId,
446
+ emptyUndoState,
447
+ pushPoint,
448
+ popPoint,
449
+ redoPoint,
450
+ compacted,
451
+ newUserMessage,
452
+ createSession,
453
+ addUserMessage,
454
+ addSnapshot,
455
+ undoTurn,
456
+ redoTurn,
457
+ addAssistantResponse,
458
+ addToolResults,
459
+ addContinuation,
460
+ addCost,
461
+ closeOpenToolCalls,
462
+ addUnreported,
463
+ budgetSpent,
464
+ pendingChildTokens,
465
+ isCostIncomplete,
466
+ addChildDebit,
467
+ addChildReport,
468
+ settleChildDebits,
469
+ addSideUsage,
470
+ gateProjectSettings
471
+ };
472
+ //# sourceMappingURL=chunk-735BKABA.js.map
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-EU7PA45O.js";
7
7
  import {
8
8
  DEFAULT_ENV_ALLOWLIST
9
- } from "./chunk-GXZACJUI.js";
9
+ } from "./chunk-B4SJY7KT.js";
10
10
 
11
11
  // src/lsp/install.ts
12
12
  import { mkdir } from "fs/promises";
@@ -70,4 +70,4 @@ export {
70
70
  installCommand,
71
71
  installServer
72
72
  };
73
- //# sourceMappingURL=chunk-B6FA4I7Q.js.map
73
+ //# sourceMappingURL=chunk-7TH3NHCQ.js.map
@@ -178,8 +178,8 @@ function parseReview(text) {
178
178
  const m = /^\s*VERDICT:\s*(ready|needs a look)\b/im.exec(text);
179
179
  return { verdict: m?.[1]?.toLowerCase() === "ready" ? "ready" : "needs-look", text: text.trim() };
180
180
  }
181
- function jobVerdict(flags, review) {
182
- if (flags.some((f) => f.level === "stop")) return "needs-look";
181
+ function jobVerdict(flags, review, reviewFailed = false) {
182
+ if (reviewFailed || flags.some((f) => f.level === "stop")) return "needs-look";
183
183
  return review ?? (flags.length === 0 ? "ready" : "needs-look");
184
184
  }
185
185
  function tail(text, lines = TEST_TAIL_LINES) {
@@ -202,4 +202,4 @@ export {
202
202
  jobVerdict,
203
203
  tail
204
204
  };
205
- //# sourceMappingURL=chunk-OUCV3FCT.js.map
205
+ //# sourceMappingURL=chunk-AOGFYUKG.js.map