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