@garuda-agent/garuda 0.18.0 → 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 (58) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +3 -2
  3. package/dist/{acpCommand-IUZPLPLI.js → acpCommand-PUJHFWYU.js} +10 -8
  4. package/dist/advise-C777SZ5M.js +433 -0
  5. package/dist/brief-RWKESOPT.js +91 -0
  6. package/dist/buildCheck-HB46NWLX.js +19 -0
  7. package/dist/buildInfo-Z4P7QAQL.js +8 -0
  8. package/dist/chunk-2OYTNNUJ.js +216 -0
  9. package/dist/chunk-4BELTWP4.js +156 -0
  10. package/dist/chunk-4QRTTRVK.js +88 -0
  11. package/dist/chunk-6JQTPW3P.js +88 -0
  12. package/dist/chunk-A5DLAXB4.js +1780 -0
  13. package/dist/{chunk-FARS4R67.js → chunk-AX3A5KPH.js} +18 -1
  14. package/dist/chunk-CFDRP6XW.js +9 -0
  15. package/dist/chunk-E6YRCDSF.js +9 -0
  16. package/dist/{chunk-OQ26W5L4.js → chunk-HP6EGFHV.js} +132 -75
  17. package/dist/chunk-L55VUSX3.js +54 -0
  18. package/dist/{chunk-B4SJY7KT.js → chunk-LMKGSYJC.js} +77 -8
  19. package/dist/{chunk-Q4P3W657.js → chunk-LWNMEGDF.js} +10 -1124
  20. package/dist/{chunk-23FXBNBB.js → chunk-MNE7HHX2.js} +2 -1
  21. package/dist/{chunk-UKIE4AAM.js → chunk-NLS7GMF7.js} +11 -105
  22. package/dist/{chunk-6XHON3Q6.js → chunk-QXTHEFKP.js} +3 -3
  23. package/dist/{chunk-4BVCYCQT.js → chunk-R7RCW4BQ.js} +9 -7
  24. package/dist/{chunk-NHWHG3SW.js → chunk-STVFWSIL.js} +350 -57
  25. package/dist/{chunk-5B5HDTUR.js → chunk-TWQ7V2L6.js} +2 -2
  26. package/dist/chunk-ULAJPMY7.js +107 -0
  27. package/dist/chunk-VISKX36R.js +306 -0
  28. package/dist/{chunk-55CFEEIN.js → chunk-WK5NE6RY.js} +2 -2
  29. package/dist/chunk-Y62J7PTD.js +90 -0
  30. package/dist/chunk-YAQ6QLKB.js +40 -0
  31. package/dist/{chunk-FOPBOB7W.js → chunk-YEH2OONM.js} +6 -4
  32. package/dist/{chunk-7TH3NHCQ.js → chunk-ZKT4P2PC.js} +2 -2
  33. package/dist/cli/index.js +242 -52
  34. package/dist/{create-W6BV75QX.js → create-OACSGJ37.js} +5 -5
  35. package/dist/{evalCommand-SKJQ6CW2.js → evalCommand-MRO6ENCA.js} +77 -39
  36. package/dist/{evalTune-CZRCPSZ3.js → evalTune-VL5RHYOG.js} +24 -18
  37. package/dist/{export-SJ3DUP6W.js → export-ZPF3AMO6.js} +27 -4
  38. package/dist/exportHtml-ISISG5HG.js +21 -0
  39. package/dist/{inkChat-WYYAFZXD.js → inkChat-UDFX6GH6.js} +256 -41
  40. package/dist/{install-TKWN5AYH.js → install-4ST5BNRG.js} +3 -3
  41. package/dist/{jobCommand-2ODQLHB3.js → jobCommand-ILOTYOX3.js} +8 -7
  42. package/dist/{launchd-3A3ONEW5.js → launchd-QG6R5RAK.js} +4 -4
  43. package/dist/{lspCommand-ZZ6R4PA2.js → lspCommand-X4FTTONR.js} +3 -3
  44. package/dist/{manager-TCYTA5XU.js → manager-YH7722C2.js} +3 -3
  45. package/dist/{nightCommand-WJU6VZA6.js → nightCommand-XPWT6CQZ.js} +8 -7
  46. package/dist/receiptView-YFDHV253.js +19 -0
  47. package/dist/{remove-42WQNVPX.js → remove-STX5UO7X.js} +3 -3
  48. package/dist/{run-PTMRQCIU.js → run-W4TK52WT.js} +5 -5
  49. package/dist/runner-EBF6EOYM.js +97 -0
  50. package/dist/{server-VEYIJHEU.js → server-ASM73QVY.js} +3 -3
  51. package/dist/tour-CEVMAC5S.js +18 -0
  52. package/dist/tune-KSNKBPNJ.js +51 -0
  53. package/dist/uiState-YFJQ43D7.js +16 -0
  54. package/package.json +1 -1
  55. package/dist/chunk-4JSUUDFQ.js +0 -118
  56. package/dist/chunk-C57GHLKP.js +0 -9
  57. package/dist/chunk-DOCDCQVM.js +0 -484
  58. package/dist/tune-YVI2KWQF.js +0 -31
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ loadLspConfig
4
+ } from "./chunk-G2AX7MGS.js";
5
+ import {
6
+ gateProjectSettings
7
+ } from "./chunk-ULAJPMY7.js";
2
8
  import {
3
9
  mergedModels,
4
10
  writeFileAtomic
5
11
  } from "./chunk-65NUEOI7.js";
6
12
  import {
7
- loadLspConfig
8
- } from "./chunk-G2AX7MGS.js";
13
+ ReceiptCollector
14
+ } from "./chunk-4QRTTRVK.js";
9
15
  import {
10
16
  hasServerBlocks,
11
17
  serverCallText,
@@ -29,17 +35,17 @@ import {
29
35
  compacted,
30
36
  createSession,
31
37
  emptyUndoState,
32
- gateProjectSettings,
33
38
  isCostIncomplete,
34
39
  newSessionId,
35
40
  newUserMessage,
41
+ pendingChildTokens,
36
42
  popPoint,
37
43
  pushPoint,
38
44
  redoPoint,
39
45
  redoTurn,
40
46
  settleChildDebits,
41
47
  undoTurn
42
- } from "./chunk-UKIE4AAM.js";
48
+ } from "./chunk-NLS7GMF7.js";
43
49
  import {
44
50
  REDACTED,
45
51
  Redactor,
@@ -57,10 +63,10 @@ import {
57
63
  } from "./chunk-B25VL4RU.js";
58
64
  import {
59
65
  BUILTIN_COMMANDS
60
- } from "./chunk-23FXBNBB.js";
66
+ } from "./chunk-MNE7HHX2.js";
61
67
  import {
62
68
  VERSION
63
- } from "./chunk-C57GHLKP.js";
69
+ } from "./chunk-CFDRP6XW.js";
64
70
  import {
65
71
  TrustStore
66
72
  } from "./chunk-5DEWP3YX.js";
@@ -91,8 +97,9 @@ import {
91
97
  expandAllowlist,
92
98
  hostAllowed,
93
99
  isSensitive,
94
- loadSettings
95
- } from "./chunk-B4SJY7KT.js";
100
+ loadSettings,
101
+ presetFor
102
+ } from "./chunk-LMKGSYJC.js";
96
103
  import {
97
104
  EFFORTS,
98
105
  ZERO_USAGE,
@@ -1502,7 +1509,12 @@ ${z.prettifyError(parsed.error)}`,
1502
1509
  await logRun(outcome.isError);
1503
1510
  const final = context.hooks === void 0 ? outcome : await context.hooks.after(hookCall, outcome, context.signal);
1504
1511
  const subagent = tool.report?.(output);
1505
- return subagent === void 0 ? final : { ...final, subagent };
1512
+ const command = tool.commandFacts?.(output);
1513
+ return {
1514
+ ...final,
1515
+ ...subagent === void 0 ? {} : { subagent },
1516
+ ...command === void 0 ? {} : { command }
1517
+ };
1506
1518
  } catch (error) {
1507
1519
  if (context.signal.aborted) throw error;
1508
1520
  if (error instanceof AuditWriteError) {
@@ -1795,7 +1807,8 @@ function createBashTool({
1795
1807
  }
1796
1808
  const result = await executor.run(run, policy, { signal });
1797
1809
  const hints = commandHints(run, stripped);
1798
- const network = sandboxed ? networkHint(permissions.takeNetworkBlocks?.() ?? [], strict) : void 0;
1810
+ const blocks = sandboxed ? permissions.takeNetworkBlocks?.() ?? [] : [];
1811
+ const network = sandboxed ? networkHint(blocks, strict) : void 0;
1799
1812
  if (network !== void 0) hints.push(network);
1800
1813
  const blocked = sandboxed && network === void 0 ? sandboxHint(result, policy.proxy !== void 0, strict) : void 0;
1801
1814
  if (blocked !== void 0) hints.push(blocked);
@@ -1810,7 +1823,21 @@ function createBashTool({
1810
1823
  `Garuda could not check for processes that left the command's process group (${result.reapProblem}). Such a process may still run, inside the sandbox.`
1811
1824
  );
1812
1825
  }
1813
- return { ...result, hints };
1826
+ return {
1827
+ ...result,
1828
+ hints,
1829
+ sandboxed,
1830
+ blockedHosts: [...new Set(blocks.map((b) => b.host))]
1831
+ };
1832
+ },
1833
+ commandFacts(result) {
1834
+ if ("daemon" in result && result.daemon !== void 0) return void 0;
1835
+ return {
1836
+ exitCode: result.exitCode,
1837
+ timedOut: result.timedOut,
1838
+ sandboxed: result.sandboxed ?? false,
1839
+ blockedHosts: result.blockedHosts ?? []
1840
+ };
1814
1841
  },
1815
1842
  toText(result) {
1816
1843
  const lines = [];
@@ -6725,7 +6752,8 @@ function rebuildState(records) {
6725
6752
  unreportedTokens: 0,
6726
6753
  childDebits: /* @__PURE__ */ new Map(),
6727
6754
  start: void 0,
6728
- undo: emptyUndoState()
6755
+ undo: emptyUndoState(),
6756
+ turns: 0
6729
6757
  };
6730
6758
  const addCost2 = (cost) => {
6731
6759
  state.costUsd = state.costUsd === void 0 || cost === void 0 ? void 0 : state.costUsd + cost;
@@ -6743,10 +6771,15 @@ function rebuildState(records) {
6743
6771
  case "user":
6744
6772
  state.messages.push(record.message);
6745
6773
  newUserMessage(state.undo);
6774
+ state.turns++;
6746
6775
  break;
6747
- case "snapshot":
6776
+ case "snapshot": {
6748
6777
  pushPoint(state.undo, record.tree, record.messages, record.prompt);
6778
+ const point = state.undo.points.at(-1);
6779
+ const at = record.at ?? Date.parse(record.t);
6780
+ if (point !== void 0 && !Number.isNaN(at)) point.at = at;
6749
6781
  break;
6782
+ }
6750
6783
  case "undo":
6751
6784
  popPoint(state.undo, state.messages, record.after);
6752
6785
  break;
@@ -6792,6 +6825,23 @@ function rebuildState(records) {
6792
6825
  case "thinking":
6793
6826
  state.thinking = record.choice;
6794
6827
  break;
6828
+ case "fork":
6829
+ state.messages = structuredClone(record.messages);
6830
+ state.turns = record.from.turn - 1;
6831
+ break;
6832
+ case "receipt": {
6833
+ const point = state.undo.points.at(-1);
6834
+ if (point !== void 0 && record.receipt.files !== void 0) {
6835
+ point.changedFiles = record.receipt.files.length;
6836
+ }
6837
+ if (point !== void 0 && record.receipt.costUsd !== void 0) {
6838
+ point.costUsd = record.receipt.costUsd;
6839
+ }
6840
+ if (record.receipt.tests?.total !== void 0) {
6841
+ state.lastTestTotal = record.receipt.tests.total;
6842
+ }
6843
+ break;
6844
+ }
6795
6845
  case "unreported":
6796
6846
  state.unreportedTokens += record.tokens;
6797
6847
  state.costIncomplete = true;
@@ -6835,6 +6885,8 @@ async function resumeSession(options) {
6835
6885
  session.childDebits = state.childDebits;
6836
6886
  session.undo = state.undo;
6837
6887
  if (state.thinking !== void 0) session.thinking = state.thinking;
6888
+ session.turns = state.turns;
6889
+ if (state.lastTestTotal !== void 0) session.lastTestTotal = state.lastTestTotal;
6838
6890
  journal.write({ type: "resume", sessionId: id2, ...options.start });
6839
6891
  closeOpenToolCalls(session);
6840
6892
  return session;
@@ -6863,6 +6915,34 @@ function repairServerBlocks(message) {
6863
6915
  return { ...message, content: withoutServerBlocks(message.content) };
6864
6916
  }
6865
6917
 
6918
+ // src/session/list.ts
6919
+ var TITLE_CHARS = 60;
6920
+ function summariseSession(id2, updated, records) {
6921
+ const prompts = records.filter((r) => r.type === "user");
6922
+ const first = prompts[0]?.message.content.find((b) => b.type === "text");
6923
+ const named = records.findLast((r) => r.type === "title");
6924
+ const line = named?.type === "title" ? named.title : (first?.type === "text" ? first.text : "").trim().split("\n")[0] ?? "";
6925
+ const state = rebuildState(records);
6926
+ return {
6927
+ id: id2,
6928
+ updated,
6929
+ title: line.length <= TITLE_CHARS ? line : `${line.slice(0, TITLE_CHARS - 1)}\u2026`,
6930
+ turns: prompts.length,
6931
+ model: state.start?.model,
6932
+ costUsd: state.costUsd,
6933
+ // A child debit with no report: its USD cost is not known (Codex review of 1370184).
6934
+ costIncomplete: isCostIncomplete(state)
6935
+ };
6936
+ }
6937
+ var MAX_TITLE_CHARS = TITLE_CHARS;
6938
+ function cleanTitle(text) {
6939
+ const one = [...text].map((c) => {
6940
+ const code = c.codePointAt(0) ?? 0;
6941
+ return code < 32 || code >= 127 && code < 160 ? " " : c;
6942
+ }).join("").replace(/\s+/g, " ").trim();
6943
+ return one.length <= MAX_TITLE_CHARS ? one : `${one.slice(0, MAX_TITLE_CHARS - 1)}\u2026`;
6944
+ }
6945
+
6866
6946
  // src/skills/load.ts
6867
6947
  import { createHash as createHash7 } from "crypto";
6868
6948
  import { lstat as lstat4, readdir as readdir5, readFile as readFile12, stat as stat7 } from "fs/promises";
@@ -8136,6 +8216,10 @@ function networkNote(entries, strict = false) {
8136
8216
  strict ? "For any other host, also run the command in the sandbox: Garuda asks the user for that host, and the result says if it was blocked." : "For any other host, also run the command in the sandbox: Garuda asks the user for that host, and the result says if it was blocked. Use outside_sandbox only when the sandbox blocks something else (a write outside the project)."
8137
8217
  ].join("\n");
8138
8218
  }
8219
+ function networkKeepHint(host2) {
8220
+ const entry = presetFor(host2) ?? host2;
8221
+ return `${host2} works for commands in this session. To keep it for this project, add "${entry}" to "network.allow" in .garuda/settings.json.`;
8222
+ }
8139
8223
  function networkConsent(entries, isolation, changed) {
8140
8224
  const lines = [
8141
8225
  changed ? "The network allowlist in .garuda/settings.json changed since you allowed it." : "This project's .garuda/settings.json opens the network for commands in the sandbox.",
@@ -8167,34 +8251,6 @@ function nodeBinary(execPath = process.execPath, path = process.env.PATH ?? "")
8167
8251
  return void 0;
8168
8252
  }
8169
8253
 
8170
- // src/session/list.ts
8171
- var TITLE_CHARS = 60;
8172
- function summariseSession(id2, updated, records) {
8173
- const prompts = records.filter((r) => r.type === "user");
8174
- const first = prompts[0]?.message.content.find((b) => b.type === "text");
8175
- const named = records.findLast((r) => r.type === "title");
8176
- const line = named?.type === "title" ? named.title : (first?.type === "text" ? first.text : "").trim().split("\n")[0] ?? "";
8177
- const state = rebuildState(records);
8178
- return {
8179
- id: id2,
8180
- updated,
8181
- title: line.length <= TITLE_CHARS ? line : `${line.slice(0, TITLE_CHARS - 1)}\u2026`,
8182
- turns: prompts.length,
8183
- model: state.start?.model,
8184
- costUsd: state.costUsd,
8185
- // A child debit with no report: its USD cost is not known (Codex review of 1370184).
8186
- costIncomplete: isCostIncomplete(state)
8187
- };
8188
- }
8189
- var MAX_TITLE_CHARS = TITLE_CHARS;
8190
- function cleanTitle(text) {
8191
- const one = [...text].map((c) => {
8192
- const code = c.codePointAt(0) ?? 0;
8193
- return code < 32 || code >= 127 && code < 160 ? " " : c;
8194
- }).join("").replace(/\s+/g, " ").trim();
8195
- return one.length <= MAX_TITLE_CHARS ? one : `${one.slice(0, MAX_TITLE_CHARS - 1)}\u2026`;
8196
- }
8197
-
8198
8254
  // src/app/sessionManager.ts
8199
8255
  async function findSession(ref, store) {
8200
8256
  const all = await store.list();
@@ -8307,6 +8363,34 @@ function filesText(changes) {
8307
8363
  if (changes.length === 0) return "no file changes";
8308
8364
  return `${changes.length} file${changes.length === 1 ? "" : "s"} changed`;
8309
8365
  }
8366
+ function rewindQuestion(kind, turn, prompt, count2, changes, isolation) {
8367
+ const lines = [`Back to the start of turn ${turn}: "${prompt}"`];
8368
+ if (changes.length === 0) lines.push("No file changes.");
8369
+ else {
8370
+ lines.push(`Files (${changes.length}):`);
8371
+ for (const c of changes.slice(0, MAX_CHANGE_LINES))
8372
+ lines.push(` ${MARKS[c.status]} ${cleanText(c.path).replace(/\n/g, " ")}`);
8373
+ if (changes.length > MAX_CHANGE_LINES) {
8374
+ lines.push(` \u2026 and ${changes.length - MAX_CHANGE_LINES} more.`);
8375
+ }
8376
+ lines.push(" (+ comes back, - is removed, ~ changes)");
8377
+ }
8378
+ lines.push("Files that .gitignore covers (for example build output) are not in undo.");
8379
+ lines.push(
8380
+ kind === "rewind" ? `This takes back ${count2} turn${count2 === 1 ? "" : "s"}: their file changes and their messages. /redo brings them back one at a time.` : "A new session starts with the conversation up to this turn. This session stays as it is: /sessions opens it again.",
8381
+ "Changes that you made to these files since then go back too."
8382
+ );
8383
+ return {
8384
+ tool: kind === "rewind" ? "undo" : "fork",
8385
+ target: { kind: "input", json: "{}" },
8386
+ preview: lines.join("\n"),
8387
+ isolation,
8388
+ title: kind === "rewind" ? `Rewind to turn ${turn}?` : `Fork a new session from turn ${turn}?`,
8389
+ question: kind === "rewind" ? "Rewind?" : "Fork?",
8390
+ choices: ["once", "deny"],
8391
+ labels: { once: kind === "rewind" ? "Yes, rewind" : "Yes, fork", deny: "No" }
8392
+ };
8393
+ }
8310
8394
 
8311
8395
  // src/app/undoCoordinator.ts
8312
8396
  var UndoCoordinator = class {
@@ -8361,6 +8445,81 @@ var UndoCoordinator = class {
8361
8445
  return `The ${kind} failed, and no file changed: ${error.message}`;
8362
8446
  }
8363
8447
  }
8448
+ /**
8449
+ * /rewind <n> (UX U6): back to the start of undo point `index` (0-based) in one step: one
8450
+ * question, one restore, then each later turn leaves the conversation as /undo does, newest
8451
+ * first. Each redo entry keeps the files after its turn, so /redo brings them back in order.
8452
+ */
8453
+ async rewind(session, index3, pendingNotes, signal) {
8454
+ const store = this.snapshots;
8455
+ if (store === void 0) return "Undo is off for this session, so /rewind cannot go back.";
8456
+ const points = session?.undo.points ?? [];
8457
+ const target = points[index3];
8458
+ if (session === void 0 || target === void 0) {
8459
+ return `There is no turn ${index3 + 1}. Type /rewind for the list.`;
8460
+ }
8461
+ try {
8462
+ const now = await store.take(signal);
8463
+ const changes = await store.changes(now, target.tree, signal);
8464
+ const count2 = points.length - index3;
8465
+ const choice = await this.approver.ask(
8466
+ rewindQuestion("rewind", index3 + 1, target.prompt, count2, changes, this.isolation),
8467
+ signal
8468
+ );
8469
+ if (choice === "deny") return "Nothing changed.";
8470
+ const afters = points.slice(index3).map((_, i) => points[index3 + i + 1]?.tree ?? now);
8471
+ await store.restore(now, target.tree, signal);
8472
+ let compacted2 = false;
8473
+ for (let i = afters.length - 1; i >= 0; i--) {
8474
+ const point = undoTurn(session, afters[i]);
8475
+ if (point !== void 0 && !point.conversation) compacted2 = true;
8476
+ }
8477
+ if (compacted2) {
8478
+ pendingNotes.push(
8479
+ `The user rewound to before the turn "${target.prompt}": the file changes of that turn and the later ones are gone. Read files again before you edit them.`
8480
+ );
8481
+ }
8482
+ return `Rewound to the start of turn ${index3 + 1} ("${target.prompt}"): ${count2} turn${count2 === 1 ? "" : "s"} taken back, ${filesText(changes)}. /redo brings them back one at a time.`;
8483
+ } catch (error) {
8484
+ if (!(error instanceof SnapshotError)) throw error;
8485
+ return `The rewind failed, and no file changed: ${error.message}`;
8486
+ }
8487
+ }
8488
+ /**
8489
+ * The files for a fork (UX U6): back to undo point `index`, after one question. The caller
8490
+ * starts the new session. Returns undefined when the user said no.
8491
+ */
8492
+ async restoreForFork(session, index3, signal) {
8493
+ const store = this.snapshots;
8494
+ if (store === void 0)
8495
+ return {
8496
+ ok: false,
8497
+ text: "Undo is off for this session, so a fork cannot restore the files."
8498
+ };
8499
+ const target = session.undo.points[index3];
8500
+ if (target === void 0)
8501
+ return { ok: false, text: `There is no turn ${index3 + 1}. Type /rewind for the list.` };
8502
+ if (!target.conversation) {
8503
+ return {
8504
+ ok: false,
8505
+ text: `Turn ${index3 + 1} is before a compaction: its messages are no longer separate, so it cannot start a fork.`
8506
+ };
8507
+ }
8508
+ try {
8509
+ const now = await store.take(signal);
8510
+ const changes = await store.changes(now, target.tree, signal);
8511
+ const choice = await this.approver.ask(
8512
+ rewindQuestion("fork", index3 + 1, target.prompt, 0, changes, this.isolation),
8513
+ signal
8514
+ );
8515
+ if (choice === "deny") return { ok: false, text: "Nothing changed." };
8516
+ await store.restore(now, target.tree, signal);
8517
+ return { ok: true };
8518
+ } catch (error) {
8519
+ if (!(error instanceof SnapshotError)) throw error;
8520
+ return { ok: false, text: `The fork failed, and no file changed: ${error.message}` };
8521
+ }
8522
+ }
8364
8523
  async undoNow(session, pendingNotes, signal) {
8365
8524
  const store = this.snapshots;
8366
8525
  if (store === void 0) return "Undo is off for this session.";
@@ -8534,6 +8693,8 @@ var Runtime = class _Runtime {
8534
8693
  startModel;
8535
8694
  /** The prompt of the turn that runs or ran last (0.9: the note after a stopped turn). */
8536
8695
  lastPrompt;
8696
+ /** The receipt of the last turn (UX U1); undefined before a turn ends, or after a failed one. */
8697
+ receipt;
8537
8698
  /** LSP diagnostics after edits are on (0.4). */
8538
8699
  lspEnabled;
8539
8700
  lspOptions;
@@ -8718,6 +8879,10 @@ var Runtime = class _Runtime {
8718
8879
  );
8719
8880
  }
8720
8881
  }
8882
+ /** The chat's look (0.18.1); the CLI applies it. */
8883
+ get uiSettings() {
8884
+ return this.settings.ui;
8885
+ }
8721
8886
  /** The notification settings for the chat (0.6); the CLI applies them. */
8722
8887
  get notificationSettings() {
8723
8888
  return this.settings.notifications;
@@ -9352,6 +9517,8 @@ ${neutralizeTags(output)}`
9352
9517
  }
9353
9518
  /** Run one turn: the user's prompt, then the loop until it stops. */
9354
9519
  async runTurn(prompt, signal) {
9520
+ const started = Date.now();
9521
+ this.receipt = void 0;
9355
9522
  this.lastPrompt = prompt;
9356
9523
  this.turnSignal = signal;
9357
9524
  await this.startHooks(signal);
@@ -9382,6 +9549,22 @@ ${neutralizeTags(output)}`
9382
9549
  ];
9383
9550
  if (lines.length > 0) this.onEvent?.({ type: "notice", text: lines.join("\n") });
9384
9551
  closeOpenToolCalls(session);
9552
+ session.turns = (session.turns ?? 0) + 1;
9553
+ const before = {
9554
+ cost: session.costUsd,
9555
+ unreported: session.unreportedTokens,
9556
+ pending: pendingChildTokens(session),
9557
+ incomplete: session.costIncomplete
9558
+ };
9559
+ const collector = new ReceiptCollector(detectTestCommand(this.root, this.profiles));
9560
+ let changesFiles = false;
9561
+ const onEvent = (event) => {
9562
+ collector.onEvent(event);
9563
+ if (event.type === "tool_call" && !this.tools.isReadOnly(event.call.name)) {
9564
+ changesFiles = true;
9565
+ }
9566
+ this.onEvent?.(event);
9567
+ };
9385
9568
  addUserMessage(
9386
9569
  session,
9387
9570
  prompt,
@@ -9407,17 +9590,84 @@ ${neutralizeTags(output)}`
9407
9590
  ...this.outputTokens(),
9408
9591
  contextWindow: this.limits.contextWindow,
9409
9592
  ...this.price === void 0 ? {} : { price: this.price },
9410
- ...this.onEvent === void 0 ? {} : { onEvent: this.onEvent },
9593
+ onEvent,
9411
9594
  ...this.settings.thinking?.keepBlocks === false ? { keepThinking: false } : {},
9412
9595
  ...this.thinkingParams === void 0 ? {} : { thinking: this.thinkingParams },
9413
9596
  signal
9414
9597
  });
9598
+ this.receipt = await this.makeReceipt(session, result, collector, {
9599
+ ...before,
9600
+ started,
9601
+ changesFiles,
9602
+ signal
9603
+ });
9415
9604
  if (this.turnMode === "plan" && result.stopReason === "done") {
9416
9605
  const plan2 = lastAssistantText(session.messages);
9417
9606
  if (plan2 !== "") this.plan = { request: prompt, plan: plan2, sessionId: session.id };
9418
9607
  }
9419
9608
  return result;
9420
9609
  }
9610
+ /** The receipt of the last turn (UX U1), or undefined. */
9611
+ get lastReceipt() {
9612
+ return this.receipt;
9613
+ }
9614
+ /**
9615
+ * The turn's receipt (UX U1; docs/lld/ux.md): the files from a snapshot at the end of the turn
9616
+ * (only when a tool that can change files ran), the commands' facts, and the costs.
9617
+ */
9618
+ async makeReceipt(session, result, collector, before) {
9619
+ const store = this.undoCoordinator.store;
9620
+ const point = session.undo.points.at(-1);
9621
+ let files;
9622
+ if (store !== void 0 && point !== void 0) {
9623
+ files = [];
9624
+ if (before.changesFiles) {
9625
+ try {
9626
+ const end = await store.take(before.signal);
9627
+ files = (await store.stats(point.tree, end, before.signal)).map((f) => ({
9628
+ path: f.path,
9629
+ ...f.added === void 0 ? {} : { added: f.added, removed: f.removed ?? 0 }
9630
+ }));
9631
+ } catch (error) {
9632
+ if (before.signal.aborted) throw error;
9633
+ files = void 0;
9634
+ }
9635
+ }
9636
+ if (files !== void 0) point.changedFiles = files.length;
9637
+ if (session.costUsd !== void 0 && before.cost !== void 0) {
9638
+ point.costUsd = session.costUsd - before.cost;
9639
+ }
9640
+ }
9641
+ const costIncomplete = session.unreportedTokens > before.unreported || pendingChildTokens(session) > before.pending || session.costIncomplete && !before.incomplete;
9642
+ const tests = collector.tests;
9643
+ if (tests !== void 0 && session.lastTestTotal !== void 0) {
9644
+ tests.previousTotal = session.lastTestTotal;
9645
+ }
9646
+ if (tests?.total !== void 0) session.lastTestTotal = tests.total;
9647
+ const receipt = {
9648
+ turn: session.turns ?? 1,
9649
+ stopReason: result.stopReason,
9650
+ steps: result.steps,
9651
+ durationMs: Date.now() - before.started,
9652
+ ...session.costUsd === void 0 || before.cost === void 0 ? {} : { costUsd: session.costUsd - before.cost },
9653
+ ...costIncomplete ? { costIncomplete: true } : {},
9654
+ ...files === void 0 ? {} : { files },
9655
+ ...tests === void 0 ? {} : { tests },
9656
+ commands: {
9657
+ sandboxed: collector.sandboxed,
9658
+ outside: collector.outside,
9659
+ blockedHosts: [...collector.blockedHosts]
9660
+ },
9661
+ contextPercent: Math.round(session.contextTokens / this.limits.contextWindow * 100),
9662
+ ...session.costUsd === void 0 ? {} : { sessionCostUsd: session.costUsd },
9663
+ ...isCostIncomplete(session) ? { sessionCostIncomplete: true } : {},
9664
+ ...store === void 0 ? {} : {
9665
+ keptChanges: session.undo.points.filter((p) => (p.changedFiles ?? 0) > 0).length
9666
+ }
9667
+ };
9668
+ session.journal?.write({ type: "receipt", receipt });
9669
+ return receipt;
9670
+ }
9421
9671
  /** The last finished plan of this process, for /schedule (0.7). */
9422
9672
  get lastPlan() {
9423
9673
  return this.plan;
@@ -9434,7 +9684,7 @@ ${neutralizeTags(output)}`
9434
9684
  text: "There is no finished plan in this chat. Make one in plan mode (/plan), then type /schedule."
9435
9685
  };
9436
9686
  }
9437
- const { createJob } = await import("./create-W6BV75QX.js");
9687
+ const { createJob } = await import("./create-OACSGJ37.js");
9438
9688
  const created = await createJob({
9439
9689
  root: this.root,
9440
9690
  executor: this.executor,
@@ -9517,7 +9767,7 @@ ${result.stderr.text}`)
9517
9767
  */
9518
9768
  async cancelJob(id2, agent = {}) {
9519
9769
  const { loadJob, saveJob } = await import("./job-7JOI4YIQ.js");
9520
- const { defaultAgentEnv, nightSpec, removeAgent, removeSpec } = await import("./launchd-3A3ONEW5.js");
9770
+ const { defaultAgentEnv, nightSpec, removeAgent, removeSpec } = await import("./launchd-QG6R5RAK.js");
9521
9771
  if (id2 === "night") {
9522
9772
  const removed = await removeSpec(
9523
9773
  agent.executor ?? this.executor,
@@ -9549,8 +9799,8 @@ ${result.stderr.text}`)
9549
9799
  */
9550
9800
  async deleteJob(id2, signal, agent = {}) {
9551
9801
  const { loadJob } = await import("./job-7JOI4YIQ.js");
9552
- const { defaultAgentEnv, removeAgent } = await import("./launchd-3A3ONEW5.js");
9553
- const { jobLeftovers, removeJob } = await import("./remove-42WQNVPX.js");
9802
+ const { defaultAgentEnv, removeAgent } = await import("./launchd-QG6R5RAK.js");
9803
+ const { jobLeftovers, removeJob } = await import("./remove-STX5UO7X.js");
9554
9804
  let job;
9555
9805
  try {
9556
9806
  job = await loadJob(this.root, id2);
@@ -9722,7 +9972,7 @@ ${result.stderr.text}`)
9722
9972
  * The files it writes are new only; the turn's own writes go through the normal approvals.
9723
9973
  */
9724
9974
  async init(signal) {
9725
- const { runInit } = await import("./run-PTMRQCIU.js");
9975
+ const { runInit } = await import("./run-W4TK52WT.js");
9726
9976
  const result = await runInit({
9727
9977
  root: this.root,
9728
9978
  home: this.commandsHome,
@@ -9757,6 +10007,8 @@ ${result.stderr.text}`)
9757
10007
  await this.staged?.remove().catch(() => {
9758
10008
  });
9759
10009
  }
10010
+ /** UX U2: the proxy runs with no host on the list, only to ask for the hosts that commands need. */
10011
+ watchingNetwork = false;
9760
10012
  /** The network allowlist in effect (0.13): the settings entries, or [] when the proxy is off. */
9761
10013
  networkAllowlist() {
9762
10014
  return this.networkProxy === void 0 ? [] : [...this.settings.network?.allow ?? []];
@@ -9768,9 +10020,11 @@ ${result.stderr.text}`)
9768
10020
  */
9769
10021
  async startNetwork(signal) {
9770
10022
  const entries = this.settings.network?.allow ?? [];
9771
- if (entries.length === 0 || this.executor.isolation !== "os") return;
10023
+ if (this.executor.isolation !== "os") return;
10024
+ const watch = entries.length === 0 && this.networkOptions.watch === true;
10025
+ if (entries.length === 0 && !watch) return;
9772
10026
  this.networkStarted ??= (async () => {
9773
- if (this.networkOptions.approved !== true) {
10027
+ if (!watch && this.networkOptions.approved !== true) {
9774
10028
  const trust = await TrustStore.open(this.networkOptions.home ?? homedir5());
9775
10029
  const hash = networkHash(entries);
9776
10030
  const known = trust.networkHash(this.root);
@@ -9790,9 +10044,11 @@ ${result.stderr.text}`)
9790
10044
  if (this.executor.name === "bwrap") {
9791
10045
  bridge = nodeBinary();
9792
10046
  if (bridge === void 0) {
9793
- this.onNotice?.(
9794
- "Network for commands: off. On Linux the allowlist needs node on the PATH (for the bridge into the sandbox)."
9795
- );
10047
+ if (!watch) {
10048
+ this.onNotice?.(
10049
+ "Network for commands: off. On Linux the allowlist needs node on the PATH (for the bridge into the sandbox)."
10050
+ );
10051
+ }
9796
10052
  return;
9797
10053
  }
9798
10054
  }
@@ -9807,6 +10063,10 @@ ${result.stderr.text}`)
9807
10063
  policy: { ...started, ...bridge === void 0 ? {} : { bridge } },
9808
10064
  takeBlocked: () => proxy.takeBlocked()
9809
10065
  });
10066
+ if (watch) {
10067
+ this.watchingNetwork = true;
10068
+ return;
10069
+ }
9810
10070
  this.onNotice?.(
9811
10071
  `Network for commands: ${entries.join(", ")}, through Garuda's proxy. Other hosts ask.`
9812
10072
  );
@@ -9840,12 +10100,15 @@ ${result.stderr.text}`)
9840
10100
  readOnly: false,
9841
10101
  info: {
9842
10102
  target: { kind: "url", url: `https://${host2}`, host: host2 },
9843
- preview: `A command in the sandbox wants to reach ${host2}:${port} through Garuda's proxy. The host is not on the network allowlist ("network.allow" in .garuda/settings.json).`,
10103
+ preview: this.watchingNetwork ? `A command in the sandbox wants to reach ${host2}:${port}. Commands have no network unless you allow a host; Garuda's proxy sees only the host name.` : `A command in the sandbox wants to reach ${host2}:${port} through Garuda's proxy. The host is not on the network allowlist ("network.allow" in .garuda/settings.json).`,
9844
10104
  title: `Let a command reach ${host2}?`
9845
10105
  }
9846
10106
  },
9847
10107
  this.turnSignal ?? new AbortController().signal
9848
10108
  );
10109
+ if (decision.allowed && decision.remembered === true) {
10110
+ this.onNotice?.(networkKeepHint(host2));
10111
+ }
9849
10112
  return decision.allowed ? { allowed: true } : { allowed: false, reason: decision.reason.replace(/\.$/, "") };
9850
10113
  }
9851
10114
  get lspHome() {
@@ -9879,7 +10142,7 @@ ${result.stderr.text}`)
9879
10142
  }
9880
10143
  /** Install a language server into ~/.garuda/lsp (the user asked for it). */
9881
10144
  async installLsp(language, signal) {
9882
- const { installServer } = await import("./install-TKWN5AYH.js");
10145
+ const { installServer } = await import("./install-4ST5BNRG.js");
9883
10146
  const result = await installServer(language, {
9884
10147
  executor: this.executor,
9885
10148
  home: this.lspHome,
@@ -9890,7 +10153,7 @@ ${result.stderr.text}`)
9890
10153
  }
9891
10154
  /** autoInstall (~/.garuda/lsp.json): ask before a managed install. */
9892
10155
  async askInstall(language, signal) {
9893
- const { installCommand } = await import("./install-TKWN5AYH.js");
10156
+ const { installCommand } = await import("./install-4ST5BNRG.js");
9894
10157
  const { LANGUAGE_LABELS } = await import("./manager-4PMSXT47.js");
9895
10158
  const { managedLabel, managedDir } = await import("./servers-6YDGWOZ6.js");
9896
10159
  const dir = managedDir(language, this.lspHome);
@@ -9923,7 +10186,7 @@ ${result.stderr.text}`)
9923
10186
  async startMcp(signal) {
9924
10187
  if (this.mcpServers.length === 0) return;
9925
10188
  this.mcpStarted ??= (async () => {
9926
- const { McpManager } = await import("./manager-TCYTA5XU.js");
10189
+ const { McpManager } = await import("./manager-YH7722C2.js");
9927
10190
  const { AuthStore } = await import("./oauth-W6OK6XXO.js");
9928
10191
  const manager = new McpManager({
9929
10192
  root: this.root,
@@ -10091,6 +10354,36 @@ ${result.stderr.text}`)
10091
10354
  async undo(signal) {
10092
10355
  return this.undoCoordinator.undo(this.current, this.pendingNotes, signal);
10093
10356
  }
10357
+ /** The undo points of this session, oldest first (UX U6: the /rewind list). */
10358
+ get rewindPoints() {
10359
+ return this.current?.undo.points ?? [];
10360
+ }
10361
+ /** /rewind <n> (UX U6): back to the start of turn n (1-based in the list). */
10362
+ async rewind(turn, signal) {
10363
+ return this.undoCoordinator.rewind(this.current, turn - 1, this.pendingNotes, signal);
10364
+ }
10365
+ /**
10366
+ * /rewind fork <n> (UX U6): a new session with the conversation up to the start of turn n, and
10367
+ * the files as they were then. The old session stays on disk as it is.
10368
+ */
10369
+ async fork(turn, signal) {
10370
+ const old = this.current;
10371
+ if (old === void 0) return "No turn has run in this session yet.";
10372
+ const restored = await this.undoCoordinator.restoreForFork(old, turn - 1, signal);
10373
+ if (!restored.ok) return restored.text;
10374
+ const point = old.undo.points[turn - 1];
10375
+ const messages = structuredClone(old.messages.slice(0, point.messages));
10376
+ this.newSession();
10377
+ const fresh = this.ensureSession();
10378
+ fresh.messages = messages;
10379
+ fresh.turns = turn - 1;
10380
+ fresh.journal?.write({ type: "fork", from: { sessionId: old.id, turn }, messages });
10381
+ fresh.journal?.write({
10382
+ type: "title",
10383
+ title: cleanTitle(`Fork of turn ${turn}: ${point.prompt}`)
10384
+ });
10385
+ return `New session ${fresh.id}: the conversation up to turn ${turn} ("${point.prompt}"), and the files as they were then. The old session ${old.id} stays as it was: /sessions opens it.`;
10386
+ }
10094
10387
  /** /redo (0.4): bring back the last undone turn: its files and its messages. */
10095
10388
  async redo(signal) {
10096
10389
  return this.undoCoordinator.redo(this.current, this.pendingNotes, signal);
@@ -10192,4 +10485,4 @@ export {
10192
10485
  modelFacts,
10193
10486
  Runtime
10194
10487
  };
10195
- //# sourceMappingURL=chunk-NHWHG3SW.js.map
10488
+ //# sourceMappingURL=chunk-STVFWSIL.js.map
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  hostCommand,
4
4
  shellWord
5
- } from "./chunk-6XHON3Q6.js";
5
+ } from "./chunk-QXTHEFKP.js";
6
6
  import {
7
7
  JOBS_DIR
8
8
  } from "./chunk-VTMD5IHD.js";
@@ -154,4 +154,4 @@ export {
154
154
  nightAgentTime,
155
155
  removeAgent
156
156
  };
157
- //# sourceMappingURL=chunk-5B5HDTUR.js.map
157
+ //# sourceMappingURL=chunk-TWQ7V2L6.js.map