reasonix 0.46.1 → 0.47.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 (139) hide show
  1. package/README.md +62 -13
  2. package/README.zh-CN.md +52 -10
  3. package/dashboard/dist/app.js +217 -60
  4. package/dashboard/dist/app.js.map +1 -1
  5. package/dist/cli/{acp-LKJU5DZX.js → acp-GEOAKSTU.js} +26 -54
  6. package/dist/cli/acp-GEOAKSTU.js.map +1 -0
  7. package/dist/cli/chat-YTPATMMG.js +51 -0
  8. package/dist/cli/{chunk-R2ASNSEO.js → chunk-2XY77LW7.js} +8 -8
  9. package/dist/cli/{chunk-SE7C5ZSI.js → chunk-4MFCAZ2W.js} +3 -3
  10. package/dist/cli/{chunk-DGA5QYFM.js → chunk-6CRPCJAU.js} +55 -13
  11. package/dist/cli/chunk-6CRPCJAU.js.map +1 -0
  12. package/dist/cli/{chunk-TDSBASOF.js → chunk-6QC5RQLE.js} +2 -2
  13. package/dist/cli/chunk-BQ6HC66J.js +530 -0
  14. package/dist/cli/chunk-BQ6HC66J.js.map +1 -0
  15. package/dist/cli/{chunk-7SGGXNB2.js → chunk-CCJAP7G3.js} +2 -2
  16. package/dist/cli/{chunk-3AAG2CUT.js → chunk-CNG32VAB.js} +2 -2
  17. package/dist/cli/{chunk-WRONKNIH.js → chunk-DN4B5S6Y.js} +2 -2
  18. package/dist/cli/{chunk-NCBP5D6E.js → chunk-DQ6K5ZQ7.js} +2 -2
  19. package/dist/cli/{chunk-MIIZJD5O.js → chunk-DWPAKZTY.js} +14 -3
  20. package/dist/cli/chunk-DWPAKZTY.js.map +1 -0
  21. package/dist/cli/{chunk-IYQ325V7.js → chunk-E5WCLUIU.js} +2 -2
  22. package/dist/cli/{chunk-YRLC2EDF.js → chunk-EQATK2L2.js} +2 -2
  23. package/dist/cli/{chunk-TEUDEGX2.js → chunk-FY4S7TJZ.js} +19 -5
  24. package/dist/cli/chunk-FY4S7TJZ.js.map +1 -0
  25. package/dist/cli/{chunk-C72TNHDE.js → chunk-GH7DC2Y5.js} +2 -2
  26. package/dist/cli/{chunk-WQ6ZRDQM.js → chunk-HIYTRCSW.js} +16 -12
  27. package/dist/cli/chunk-HIYTRCSW.js.map +1 -0
  28. package/dist/cli/{chunk-EAOL43HB.js → chunk-HUILPCYX.js} +3 -3
  29. package/dist/cli/{chunk-ZOQHVQON.js → chunk-JBH5RM7X.js} +473 -87
  30. package/dist/cli/chunk-JBH5RM7X.js.map +1 -0
  31. package/dist/cli/{chunk-XPAUNFOL.js → chunk-KVZZ5U75.js} +3 -2
  32. package/dist/cli/chunk-KVZZ5U75.js.map +1 -0
  33. package/dist/cli/{chunk-2AASOSD5.js → chunk-KYQVQ5X4.js} +85 -10
  34. package/dist/cli/chunk-KYQVQ5X4.js.map +1 -0
  35. package/dist/cli/{chunk-2425HK6U.js → chunk-LGEKVMMV.js} +7 -2
  36. package/dist/cli/{chunk-2425HK6U.js.map → chunk-LGEKVMMV.js.map} +1 -1
  37. package/dist/cli/{chunk-6VANO7KB.js → chunk-NRQ5UP5T.js} +165 -24
  38. package/dist/cli/chunk-NRQ5UP5T.js.map +1 -0
  39. package/dist/cli/{chunk-M4E5JK6S.js → chunk-QCFLPSPH.js} +2 -2
  40. package/dist/cli/{chunk-E7TAHQ4A.js → chunk-RRXUIPWG.js} +19 -18
  41. package/dist/cli/chunk-RRXUIPWG.js.map +1 -0
  42. package/dist/cli/{chunk-JLQDNLZF.js → chunk-T5A7EY6B.js} +26 -14
  43. package/dist/cli/chunk-T5A7EY6B.js.map +1 -0
  44. package/dist/cli/{chunk-7LOJS3LV.js → chunk-TDHXB2ER.js} +2 -2
  45. package/dist/cli/{chunk-CXVWUPA3.js → chunk-TKVXTQ3T.js} +26 -26
  46. package/dist/cli/chunk-TKVXTQ3T.js.map +1 -0
  47. package/dist/cli/{chunk-JVFEJAJX.js → chunk-TRSAHHCL.js} +107 -11
  48. package/dist/cli/chunk-TRSAHHCL.js.map +1 -0
  49. package/dist/cli/{chunk-K3AIFMI6.js → chunk-TRWHTFG7.js} +2 -2
  50. package/dist/cli/{chunk-7YW6TPXK.js → chunk-XD6P7AFH.js} +28 -31
  51. package/dist/cli/chunk-XD6P7AFH.js.map +1 -0
  52. package/dist/cli/{chunk-SPXN5JIT.js → chunk-XMHP7BEE.js} +1787 -1081
  53. package/dist/cli/chunk-XMHP7BEE.js.map +1 -0
  54. package/dist/cli/{chunk-JVQT5IYP.js → chunk-YFP3MYMY.js} +19 -9
  55. package/dist/cli/chunk-YFP3MYMY.js.map +1 -0
  56. package/dist/cli/{chunk-HNXDZGC6.js → chunk-ZXSCAODE.js} +9 -9
  57. package/dist/cli/{chunk-HNXDZGC6.js.map → chunk-ZXSCAODE.js.map} +1 -1
  58. package/dist/cli/{code-2JIHL5M2.js → code-Q4NRVEDG.js} +42 -35
  59. package/dist/cli/code-Q4NRVEDG.js.map +1 -0
  60. package/dist/cli/{commands-OPT5AJNH.js → commands-4CDI4GFM.js} +4 -4
  61. package/dist/cli/{commit-KA37H6GM.js → commit-GW7LDQP5.js} +3 -3
  62. package/dist/cli/{desktop-5ONTRU3C.js → desktop-EG6P5SF2.js} +321 -36
  63. package/dist/cli/desktop-EG6P5SF2.js.map +1 -0
  64. package/dist/cli/{diff-SOIA7AKH.js → diff-VI2YX4FN.js} +8 -8
  65. package/dist/cli/{doctor-RCUP4XRV.js → doctor-CQTTZP27.js} +9 -9
  66. package/dist/cli/{events-6KHITNX4.js → events-VRYXOSKI.js} +3 -3
  67. package/dist/cli/index.js +94 -45
  68. package/dist/cli/index.js.map +1 -1
  69. package/dist/cli/{mcp-JP5OWD6R.js → mcp-J2UCD4RZ.js} +2 -2
  70. package/dist/cli/{mcp-browse-ONCJJPJN.js → mcp-browse-GSX34JEK.js} +2 -2
  71. package/dist/cli/{mcp-inspect-TPLHW5JA.js → mcp-inspect-RRFYF4ZV.js} +4 -4
  72. package/dist/cli/{prompt-RJDNCQAP.js → prompt-5TQPIVHV.js} +4 -4
  73. package/dist/cli/{prune-sessions-MKEATRVL.js → prune-sessions-SEWX7GP6.js} +2 -2
  74. package/dist/cli/{replay-4NILJG4U.js → replay-MJCEMODU.js} +9 -9
  75. package/dist/cli/{run-WFGXB4SB.js → run-P4D5VDYE.js} +17 -17
  76. package/dist/cli/{server-5VFQP3PV.js → server-C25JNNZV.js} +82 -34
  77. package/dist/cli/server-C25JNNZV.js.map +1 -0
  78. package/dist/cli/{sessions-5XDJDALO.js → sessions-QIONZJQ6.js} +15 -15
  79. package/dist/cli/{setup-F6XSWLRA.js → setup-NLQ6G5G4.js} +7 -7
  80. package/dist/cli/setup-NLQ6G5G4.js.map +1 -0
  81. package/dist/cli/{stats-ALHBZICE.js → stats-DFZEXHP4.js} +6 -6
  82. package/dist/cli/{version-JVRAHBMM.js → version-GR3X3MPI.js} +15 -15
  83. package/dist/index.d.ts +69 -56
  84. package/dist/index.js +791 -303
  85. package/dist/index.js.map +1 -1
  86. package/package.json +3 -1
  87. package/dist/cli/acp-LKJU5DZX.js.map +0 -1
  88. package/dist/cli/chat-W7LAWEN6.js +0 -51
  89. package/dist/cli/chunk-2AASOSD5.js.map +0 -1
  90. package/dist/cli/chunk-6VANO7KB.js.map +0 -1
  91. package/dist/cli/chunk-7YW6TPXK.js.map +0 -1
  92. package/dist/cli/chunk-CXVWUPA3.js.map +0 -1
  93. package/dist/cli/chunk-DGA5QYFM.js.map +0 -1
  94. package/dist/cli/chunk-DHRVZJ2D.js +0 -642
  95. package/dist/cli/chunk-DHRVZJ2D.js.map +0 -1
  96. package/dist/cli/chunk-E7TAHQ4A.js.map +0 -1
  97. package/dist/cli/chunk-JLQDNLZF.js.map +0 -1
  98. package/dist/cli/chunk-JVFEJAJX.js.map +0 -1
  99. package/dist/cli/chunk-JVQT5IYP.js.map +0 -1
  100. package/dist/cli/chunk-MIIZJD5O.js.map +0 -1
  101. package/dist/cli/chunk-SPXN5JIT.js.map +0 -1
  102. package/dist/cli/chunk-TEUDEGX2.js.map +0 -1
  103. package/dist/cli/chunk-WQ6ZRDQM.js.map +0 -1
  104. package/dist/cli/chunk-XPAUNFOL.js.map +0 -1
  105. package/dist/cli/chunk-ZOQHVQON.js.map +0 -1
  106. package/dist/cli/code-2JIHL5M2.js.map +0 -1
  107. package/dist/cli/desktop-5ONTRU3C.js.map +0 -1
  108. package/dist/cli/server-5VFQP3PV.js.map +0 -1
  109. package/dist/cli/setup-F6XSWLRA.js.map +0 -1
  110. /package/dist/cli/{chat-W7LAWEN6.js.map → chat-YTPATMMG.js.map} +0 -0
  111. /package/dist/cli/{chunk-R2ASNSEO.js.map → chunk-2XY77LW7.js.map} +0 -0
  112. /package/dist/cli/{chunk-SE7C5ZSI.js.map → chunk-4MFCAZ2W.js.map} +0 -0
  113. /package/dist/cli/{chunk-TDSBASOF.js.map → chunk-6QC5RQLE.js.map} +0 -0
  114. /package/dist/cli/{chunk-7SGGXNB2.js.map → chunk-CCJAP7G3.js.map} +0 -0
  115. /package/dist/cli/{chunk-3AAG2CUT.js.map → chunk-CNG32VAB.js.map} +0 -0
  116. /package/dist/cli/{chunk-WRONKNIH.js.map → chunk-DN4B5S6Y.js.map} +0 -0
  117. /package/dist/cli/{chunk-NCBP5D6E.js.map → chunk-DQ6K5ZQ7.js.map} +0 -0
  118. /package/dist/cli/{chunk-IYQ325V7.js.map → chunk-E5WCLUIU.js.map} +0 -0
  119. /package/dist/cli/{chunk-YRLC2EDF.js.map → chunk-EQATK2L2.js.map} +0 -0
  120. /package/dist/cli/{chunk-C72TNHDE.js.map → chunk-GH7DC2Y5.js.map} +0 -0
  121. /package/dist/cli/{chunk-EAOL43HB.js.map → chunk-HUILPCYX.js.map} +0 -0
  122. /package/dist/cli/{chunk-M4E5JK6S.js.map → chunk-QCFLPSPH.js.map} +0 -0
  123. /package/dist/cli/{chunk-7LOJS3LV.js.map → chunk-TDHXB2ER.js.map} +0 -0
  124. /package/dist/cli/{chunk-K3AIFMI6.js.map → chunk-TRWHTFG7.js.map} +0 -0
  125. /package/dist/cli/{commands-OPT5AJNH.js.map → commands-4CDI4GFM.js.map} +0 -0
  126. /package/dist/cli/{commit-KA37H6GM.js.map → commit-GW7LDQP5.js.map} +0 -0
  127. /package/dist/cli/{diff-SOIA7AKH.js.map → diff-VI2YX4FN.js.map} +0 -0
  128. /package/dist/cli/{doctor-RCUP4XRV.js.map → doctor-CQTTZP27.js.map} +0 -0
  129. /package/dist/cli/{events-6KHITNX4.js.map → events-VRYXOSKI.js.map} +0 -0
  130. /package/dist/cli/{mcp-JP5OWD6R.js.map → mcp-J2UCD4RZ.js.map} +0 -0
  131. /package/dist/cli/{mcp-browse-ONCJJPJN.js.map → mcp-browse-GSX34JEK.js.map} +0 -0
  132. /package/dist/cli/{mcp-inspect-TPLHW5JA.js.map → mcp-inspect-RRFYF4ZV.js.map} +0 -0
  133. /package/dist/cli/{prompt-RJDNCQAP.js.map → prompt-5TQPIVHV.js.map} +0 -0
  134. /package/dist/cli/{prune-sessions-MKEATRVL.js.map → prune-sessions-SEWX7GP6.js.map} +0 -0
  135. /package/dist/cli/{replay-4NILJG4U.js.map → replay-MJCEMODU.js.map} +0 -0
  136. /package/dist/cli/{run-WFGXB4SB.js.map → run-P4D5VDYE.js.map} +0 -0
  137. /package/dist/cli/{sessions-5XDJDALO.js.map → sessions-QIONZJQ6.js.map} +0 -0
  138. /package/dist/cli/{stats-ALHBZICE.js.map → stats-DFZEXHP4.js.map} +0 -0
  139. /package/dist/cli/{version-JVRAHBMM.js.map → version-GR3X3MPI.js.map} +0 -0
@@ -3,7 +3,7 @@ import { createRequire as __cr } from 'node:module'; if (typeof globalThis.requi
3
3
  import {
4
4
  MemoryStore,
5
5
  sanitizeMemoryName
6
- } from "./chunk-DHRVZJ2D.js";
6
+ } from "./chunk-BQ6HC66J.js";
7
7
  import {
8
8
  countTokens,
9
9
  countTokensBounded,
@@ -12,23 +12,23 @@ import {
12
12
  } from "./chunk-6OWJV3YW.js";
13
13
  import {
14
14
  Usage
15
- } from "./chunk-MIIZJD5O.js";
15
+ } from "./chunk-DWPAKZTY.js";
16
16
  import {
17
17
  applyEdit,
18
18
  applyMultiEdit,
19
19
  pauseGate
20
- } from "./chunk-JVFEJAJX.js";
20
+ } from "./chunk-TRSAHHCL.js";
21
21
  import {
22
22
  NEGATIVE_CLAIM_RULE,
23
23
  PROJECT_MEMORY_FILES,
24
24
  PROJECT_MEMORY_MAX_CHARS,
25
25
  TUI_FORMATTING_RULES,
26
26
  memoryEnabled
27
- } from "./chunk-TEUDEGX2.js";
27
+ } from "./chunk-FY4S7TJZ.js";
28
28
  import {
29
29
  formatHookOutcomeMessage,
30
30
  runHooks
31
- } from "./chunk-C72TNHDE.js";
31
+ } from "./chunk-GH7DC2Y5.js";
32
32
  import {
33
33
  ignoredByLayers,
34
34
  loadGitignoreAt,
@@ -41,25 +41,26 @@ import {
41
41
  loadSessionMeta,
42
42
  rewriteSession,
43
43
  timestampSuffix
44
- } from "./chunk-E7TAHQ4A.js";
44
+ } from "./chunk-RRXUIPWG.js";
45
45
  import {
46
46
  DEEPSEEK_CONTEXT_TOKENS,
47
47
  DEFAULT_CONTEXT_TOKENS,
48
48
  SessionStats
49
- } from "./chunk-M4E5JK6S.js";
49
+ } from "./chunk-QCFLPSPH.js";
50
50
  import {
51
51
  t
52
- } from "./chunk-6VANO7KB.js";
52
+ } from "./chunk-NRQ5UP5T.js";
53
53
  import {
54
54
  DEFAULT_INDEX_EXCLUDES,
55
55
  addProjectPathAllowed,
56
56
  loadMemoryTypeRegistry,
57
57
  loadMetasoApiKey,
58
58
  loadProjectPathAllowed,
59
+ loadTavilyApiKey,
59
60
  require_picomatch,
60
61
  webSearchEndpoint,
61
62
  webSearchEngine
62
- } from "./chunk-DGA5QYFM.js";
63
+ } from "./chunk-6CRPCJAU.js";
63
64
  import {
64
65
  __commonJS,
65
66
  __esm,
@@ -6124,11 +6125,42 @@ async function bridgeMcpTools(client, opts = {}) {
6124
6125
  return { ...result, env };
6125
6126
  }
6126
6127
  function flattenMcpResult(result, opts = {}) {
6128
+ validateResultShape(result);
6127
6129
  const parts = result.content.map(blockToString);
6128
6130
  const joined = parts.join("\n").trim();
6129
6131
  const prefixed = result.isError ? `ERROR: ${joined || "(no error message from server)"}` : joined;
6130
6132
  return opts.maxChars ? truncateForModel(prefixed, opts.maxChars) : prefixed;
6131
6133
  }
6134
+ function validateResultShape(result) {
6135
+ if (typeof result !== "object" || !result)
6136
+ throw new Error(`MCP server returned non-object result: ${typeof result}`);
6137
+ const { content, isError: _isError } = result;
6138
+ if (!Array.isArray(content))
6139
+ throw new Error(`MCP server returned result with non-array content: ${typeof content}`);
6140
+ for (let i = 0; i < content.length; i++) {
6141
+ const block = content[i];
6142
+ if (typeof block !== "object" || !block)
6143
+ throw new Error(`MCP server returned result.content[${i}] is not an object`);
6144
+ if (block.type !== "text" && block.type !== "image")
6145
+ throw new Error(
6146
+ `MCP server returned result.content[${i}] with unknown type ${JSON.stringify(block.type)}`
6147
+ );
6148
+ if (block.type === "text" && typeof block.text !== "string")
6149
+ throw new Error(
6150
+ `MCP server returned result.content[${i}] with non-string text (${typeof block.text})`
6151
+ );
6152
+ if (block.type === "image") {
6153
+ if (typeof block.data !== "string")
6154
+ throw new Error(
6155
+ `MCP server returned result.content[${i}] with non-string data (${typeof block.data})`
6156
+ );
6157
+ if (typeof block.mimeType !== "string")
6158
+ throw new Error(
6159
+ `MCP server returned result.content[${i}] with non-string mimeType (${typeof block.mimeType})`
6160
+ );
6161
+ }
6162
+ }
6163
+ }
6132
6164
  function truncateForModel(s, maxChars) {
6133
6165
  if (s.length <= maxChars) return s;
6134
6166
  const tailBudget = Math.min(1024, Math.floor(maxChars * 0.1));
@@ -6277,10 +6309,13 @@ var ToolRegistry = class {
6277
6309
  _autoFlatten;
6278
6310
  _planMode = false;
6279
6311
  _interceptor = null;
6312
+ _interceptors = [];
6280
6313
  _auditListener = null;
6281
6314
  _resultAugmenter = null;
6282
6315
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
6283
6316
  _lastMalformed = /* @__PURE__ */ new Map();
6317
+ /** Per-tool fingerprint of the last host-side interceptor rejection. */
6318
+ _lastInterceptorRejection = /* @__PURE__ */ new Map();
6284
6319
  constructor(opts = {}) {
6285
6320
  this._autoFlatten = opts.autoFlatten !== false;
6286
6321
  }
@@ -6296,6 +6331,18 @@ var ToolRegistry = class {
6296
6331
  setToolInterceptor(fn) {
6297
6332
  this._interceptor = fn;
6298
6333
  }
6334
+ /** Ordered host-side interceptors. They run before the legacy single interceptor. */
6335
+ addToolInterceptor(id, fn) {
6336
+ const normalized = id.trim();
6337
+ if (!normalized) throw new Error("tool interceptor requires a non-empty id");
6338
+ const existing = this._interceptors.findIndex((entry) => entry.id === normalized);
6339
+ if (existing >= 0) this._interceptors.splice(existing, 1);
6340
+ this._interceptors.push({ id: normalized, fn });
6341
+ return () => {
6342
+ const idx = this._interceptors.findIndex((entry) => entry.id === normalized);
6343
+ if (idx >= 0) this._interceptors.splice(idx, 1);
6344
+ };
6345
+ }
6299
6346
  setAuditListener(fn) {
6300
6347
  this._auditListener = fn;
6301
6348
  }
@@ -6384,16 +6431,27 @@ var ToolRegistry = class {
6384
6431
  rejectedReason: "plan-mode"
6385
6432
  });
6386
6433
  }
6387
- if (this._interceptor) {
6434
+ const chain = this._interceptor ? [...this._interceptors.map((entry) => entry.fn), this._interceptor] : this._interceptors.map((entry) => entry.fn);
6435
+ for (const interceptor of chain) {
6388
6436
  try {
6389
- const short = await this._interceptor(name, args);
6390
- if (typeof short === "string") return short;
6437
+ const short = await interceptor(name, args);
6438
+ if (typeof short === "string") {
6439
+ const guarded = this._noteInterceptorRejection(name, fingerprint, short);
6440
+ return this._augmentResult(name, args, guarded);
6441
+ }
6391
6442
  } catch (err) {
6392
6443
  return JSON.stringify({
6393
6444
  error: `${name}: interceptor failed \u2014 ${err.message}`
6394
6445
  });
6395
6446
  }
6396
6447
  }
6448
+ this._lastInterceptorRejection.delete(name);
6449
+ if (opts.signal?.aborted) {
6450
+ return JSON.stringify({
6451
+ error: `${name}: aborted before dispatch (user interrupt)`,
6452
+ rejectedReason: "aborted"
6453
+ });
6454
+ }
6397
6455
  let finalResult;
6398
6456
  try {
6399
6457
  try {
@@ -6425,13 +6483,16 @@ var ToolRegistry = class {
6425
6483
  finalResult = JSON.stringify({ error: `${e.name}: ${e.message}` });
6426
6484
  }
6427
6485
  }
6486
+ return this._augmentResult(name, args, finalResult);
6487
+ }
6488
+ _augmentResult(name, args, result) {
6428
6489
  if (this._resultAugmenter) {
6429
6490
  try {
6430
- return this._resultAugmenter(name, args, finalResult);
6491
+ return this._resultAugmenter(name, args, result);
6431
6492
  } catch {
6432
6493
  }
6433
6494
  }
6434
- return finalResult;
6495
+ return result;
6435
6496
  }
6436
6497
  /** Records the failed call's fingerprint; on the 2nd consecutive identical malformed call to the same tool, returns a sharper error that tells the model to stop retrying. */
6437
6498
  _noteMalformed(name, fingerprint, detail) {
@@ -6445,7 +6506,35 @@ var ToolRegistry = class {
6445
6506
  }
6446
6507
  return JSON.stringify({ error: `${name}: ${detail}` });
6447
6508
  }
6509
+ _noteInterceptorRejection(name, fingerprint, result) {
6510
+ const reason = rejectedReason(result);
6511
+ if (!reason) {
6512
+ this._lastInterceptorRejection.delete(name);
6513
+ return result;
6514
+ }
6515
+ const key = `${reason}:${fingerprint}`;
6516
+ const prev = this._lastInterceptorRejection.get(name);
6517
+ this._lastInterceptorRejection.set(name, key);
6518
+ if (prev === key) {
6519
+ return JSON.stringify({
6520
+ error: `${name}: same call was just rejected by ${reason} \u2014 do not retry identical args. Switch to read-only exploration, submit or revise the plan, or choose a different tool call.`,
6521
+ rejectedReason: reason,
6522
+ consecutiveInterceptorRejection: true
6523
+ });
6524
+ }
6525
+ return result;
6526
+ }
6448
6527
  };
6528
+ function rejectedReason(result) {
6529
+ try {
6530
+ const parsed = JSON.parse(result);
6531
+ if (!parsed || typeof parsed !== "object") return null;
6532
+ const reason = parsed.rejectedReason;
6533
+ return typeof reason === "string" && reason ? reason : null;
6534
+ } catch {
6535
+ return null;
6536
+ }
6537
+ }
6449
6538
  function isReadOnlyCall(tool, args) {
6450
6539
  if (tool.readOnlyCheck) {
6451
6540
  try {
@@ -7877,6 +7966,32 @@ ${reason}`
7877
7966
  }
7878
7967
  return userText;
7879
7968
  }
7969
+ /** Rewind to the N-th user turn (0-indexed). Drops that turn + everything after. */
7970
+ rewindToUserTurn(userTurnIndex) {
7971
+ const entries = this.log.entries;
7972
+ let count = 0;
7973
+ let targetIdx = -1;
7974
+ for (let i = 0; i < entries.length; i++) {
7975
+ if (entries[i].role !== "user") continue;
7976
+ if (count === userTurnIndex) {
7977
+ targetIdx = i;
7978
+ break;
7979
+ }
7980
+ count++;
7981
+ }
7982
+ if (targetIdx < 0) return null;
7983
+ const raw = entries[targetIdx].content;
7984
+ const userText = typeof raw === "string" ? raw : "";
7985
+ const preserved = entries.slice(0, targetIdx).map((m) => ({ ...m }));
7986
+ this.log.compactInPlace(preserved);
7987
+ if (this.sessionName) {
7988
+ try {
7989
+ rewriteSession(this.sessionName, preserved);
7990
+ } catch {
7991
+ }
7992
+ }
7993
+ return userText;
7994
+ }
7880
7995
  async *step(userInput) {
7881
7996
  this._steerConsumed = false;
7882
7997
  if (this.budgetUsd !== null) {
@@ -8934,7 +9049,7 @@ function registerMemoryTools(registry, opts = {}) {
8934
9049
  }
8935
9050
  registry.register({
8936
9051
  name: "remember",
8937
- description: "Save a memory for future sessions. Use when the user states a preference, corrects your approach, shares a non-obvious fact about this project, or explicitly asks you to remember something. Don't remember transient task state \u2014 only things worth recalling next session. The memory is written now but won't re-load into the system prompt until the next `/new` or launch.",
9052
+ description: "Save a memory for future sessions \u2014 preferences, corrections, non-obvious project facts. Not for transient task state. Loads into the system prompt on next `/new` or launch.",
8938
9053
  parameters: {
8939
9054
  type: "object",
8940
9055
  properties: {
@@ -8945,29 +9060,29 @@ function registerMemoryTools(registry, opts = {}) {
8945
9060
  scope: {
8946
9061
  type: "string",
8947
9062
  enum: ["global", "project"],
8948
- description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
9063
+ description: "global = across all projects; project = current sandbox only (needs `reasonix code`)."
8949
9064
  },
8950
9065
  name: {
8951
9066
  type: "string",
8952
- description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
9067
+ description: "Filename-safe id, 3-40 chars, alnum + _ - . (no separators, no leading dot)."
8953
9068
  },
8954
9069
  description: {
8955
9070
  type: "string",
8956
- description: "One-line summary shown in MEMORY.md (under ~150 chars)."
9071
+ description: "\u2264150 char one-liner shown in MEMORY.md."
8957
9072
  },
8958
9073
  content: {
8959
9074
  type: "string",
8960
- description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
9075
+ description: "Markdown body. For feedback/project, structure as rule + **Why:** + **How to apply:**."
8961
9076
  },
8962
9077
  priority: {
8963
9078
  type: "string",
8964
9079
  enum: ["low", "medium", "high"],
8965
- description: "Optional per-memory priority. `high` injects the entry into a `# HIGH PRIORITY constraints` block at the top of the system prompt \u2014 use sparingly, only for hard rules the model must never violate."
9080
+ description: "`high` injects entry into HIGH PRIORITY block \u2014 use sparingly."
8966
9081
  },
8967
9082
  expires: {
8968
9083
  type: "string",
8969
9084
  enum: ["project_end"],
8970
- description: "Optional lifecycle hint. `project_end` causes `/memory clear project` to also remove this entry even when it's stored at global scope."
9085
+ description: "`project_end` lets /memory clear project remove this even at global scope."
8971
9086
  }
8972
9087
  },
8973
9088
  required: ["type", "scope", "name", "description", "content"]
@@ -9084,26 +9199,26 @@ function sanitizeOptions(raw) {
9084
9199
  function registerChoiceTool(registry, opts = {}) {
9085
9200
  registry.register({
9086
9201
  name: "ask_choice",
9087
- description: "Present 2\u20136 alternatives to the user. The principle: if the user is supposed to pick, the tool picks \u2014 you don't enumerate the choices as prose. Prose menus have no picker in this TUI, so the user gets a wall of text to scroll through and a letter to type, strictly worse than the magenta picker this tool renders. Call it whenever (a) the user has asked for options, (b) you've analyzed multiple approaches and the final call is theirs, or (c) it's a preference fork you can't resolve without them. Skip it when one option is clearly best (just do it, or submit_plan) or a free-form text answer fits (ask in prose). Keep option ids short and stable (A/B/C). Each option: title + optional summary. allowCustom=true when their real answer might not fit. Max 6 options \u2014 narrow first if more. A one-sentence lead-in before the call is fine; don't repeat the options in it.",
9202
+ description: "Render an arrow-key picker with 2\u20136 alternatives. Use when the user is supposed to pick \u2014 never enumerate choices as prose. Skip when one option is clearly best (just do it) or a free-form text answer fits. Max 6 options; set `allowCustom:true` when their real answer might not fit.",
9088
9203
  readOnly: true,
9089
9204
  parameters: {
9090
9205
  type: "object",
9091
9206
  properties: {
9092
9207
  question: {
9093
9208
  type: "string",
9094
- description: "The question to put in front of the user. One sentence. Don't repeat the options in the question text \u2014 the picker renders them separately."
9209
+ description: "One-sentence question. Don't repeat the options here \u2014 the picker renders them."
9095
9210
  },
9096
9211
  options: {
9097
9212
  type: "array",
9098
- description: "2\u20134 alternatives. Each needs a stable id and a short title; summary is optional.",
9213
+ description: "2\u20136 alternatives. Each: stable id + short title; summary optional.",
9099
9214
  items: {
9100
9215
  type: "object",
9101
9216
  properties: {
9102
- id: { type: "string", description: "Short stable id (A, B, C, or option-1)." },
9103
- title: { type: "string", description: "One-line title shown as the option label." },
9217
+ id: { type: "string", description: "Stable id (A, B, C or option-1)." },
9218
+ title: { type: "string", description: "One-line label." },
9104
9219
  summary: {
9105
9220
  type: "string",
9106
- description: "Optional. A second dimmed line with more detail. Keep under ~80 chars."
9221
+ description: "Optional dimmed second line, \u226480 chars."
9107
9222
  }
9108
9223
  },
9109
9224
  required: ["id", "title"]
@@ -9111,7 +9226,7 @@ function registerChoiceTool(registry, opts = {}) {
9111
9226
  },
9112
9227
  allowCustom: {
9113
9228
  type: "boolean",
9114
- description: "If true, the picker shows a 'Let me type my own answer' escape hatch. Default false. Turn on when the user's real answer might not fit any of your pre-defined options."
9229
+ description: "Shows a 'type my own answer' escape hatch. Default false."
9115
9230
  }
9116
9231
  },
9117
9232
  required: ["question", "options"]
@@ -9157,6 +9272,7 @@ var FETCH_MAX_BYTES = 10 * 1024 * 1024;
9157
9272
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
9158
9273
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
9159
9274
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
9275
+ var TAVILY_ENDPOINT = "https://api.tavily.com/search";
9160
9276
  function searchStatusError(status) {
9161
9277
  if (status === 429) return t("webErrors.rateLimit429");
9162
9278
  if (status === 403) return t("webErrors.forbidden403");
@@ -9176,6 +9292,9 @@ async function webSearch(query, opts = {}) {
9176
9292
  if (opts.engine === "searxng") {
9177
9293
  return searchSearxng(query, opts);
9178
9294
  }
9295
+ if (opts.engine === "tavily") {
9296
+ return searchTavily(query, opts);
9297
+ }
9179
9298
  return searchMojeek(query, opts);
9180
9299
  }
9181
9300
  async function searchMojeek(query, opts = {}) {
@@ -9310,6 +9429,55 @@ async function searchMetaso(query, opts = {}) {
9310
9429
  snippet: wp.snippet ?? wp.summary ?? ""
9311
9430
  }));
9312
9431
  }
9432
+ async function searchTavily(query, opts = {}) {
9433
+ const topK = Math.max(1, Math.min(20, opts.topK ?? DEFAULT_TOPK));
9434
+ const apiKey = loadTavilyApiKey();
9435
+ if (!apiKey) throw new Error(t("webErrors.tavilyMissingKey"));
9436
+ let resp;
9437
+ try {
9438
+ resp = await fetch(TAVILY_ENDPOINT, {
9439
+ method: "POST",
9440
+ headers: {
9441
+ "Content-Type": "application/json",
9442
+ Accept: "application/json"
9443
+ },
9444
+ body: JSON.stringify({
9445
+ api_key: apiKey,
9446
+ query,
9447
+ search_depth: "basic",
9448
+ max_results: topK,
9449
+ include_answer: false,
9450
+ include_raw_content: false,
9451
+ include_images: false
9452
+ }),
9453
+ signal: opts.signal
9454
+ });
9455
+ } catch (err) {
9456
+ if (err instanceof TypeError && err.message.includes("fetch")) {
9457
+ throw new Error(t("webErrors.cannotReach", { endpoint: TAVILY_ENDPOINT }));
9458
+ }
9459
+ throw err;
9460
+ }
9461
+ if (!resp.ok) {
9462
+ if (resp.status === 401 || resp.status === 403) {
9463
+ throw new Error(t("webErrors.tavilyUnauthorized"));
9464
+ }
9465
+ if (resp.status === 429) throw new Error(t("webErrors.tavilyRateLimit"));
9466
+ throw new Error(t("webErrors.tavilyServerError", { status: resp.status }));
9467
+ }
9468
+ let data;
9469
+ try {
9470
+ data = await resp.json();
9471
+ } catch {
9472
+ throw new Error(t("webErrors.tavilyParseError", { status: resp.status }));
9473
+ }
9474
+ const results = data.results ?? [];
9475
+ return results.slice(0, topK).map((r) => ({
9476
+ title: r.title,
9477
+ url: r.url,
9478
+ snippet: r.content ?? ""
9479
+ }));
9480
+ }
9313
9481
  function parseSearxngHtmlResults(html) {
9314
9482
  const root = (0, import_node_html_parser.parse)(html);
9315
9483
  const results = [];
@@ -9528,7 +9696,7 @@ function registerWebTools(registry, opts = {}) {
9528
9696
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
9529
9697
  registry.register({
9530
9698
  name: "web_search",
9531
- description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this. To change the backend, use /search-engine mojeek|searxng|metaso.",
9699
+ description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this. To change the backend, use /search-engine mojeek|searxng|metaso|tavily.",
9532
9700
  readOnly: true,
9533
9701
  parallelSafe: true,
9534
9702
  parameters: {
@@ -9543,8 +9711,8 @@ function registerWebTools(registry, opts = {}) {
9543
9711
  required: ["query"]
9544
9712
  },
9545
9713
  fn: async (args, ctx) => {
9546
- const engine = opts.webSearchEngine ?? webSearchEngine();
9547
- const endpoint = opts.webSearchEndpoint ?? webSearchEndpoint();
9714
+ const engine = webSearchEngine();
9715
+ const endpoint = webSearchEndpoint();
9548
9716
  const results = await webSearch(args.query, {
9549
9717
  topK: args.topK ?? defaultTopK,
9550
9718
  signal: ctx?.signal,
@@ -9600,13 +9768,13 @@ import * as pathMod4 from "path";
9600
9768
  // src/memory/subdir.ts
9601
9769
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9602
9770
  import { dirname, join as join2, relative as relative2, resolve as resolve2 } from "path";
9603
- function findSubdirMemoryAncestors(absPath, rootDir) {
9771
+ function findDirMemory(absDir, rootDir) {
9604
9772
  const root = resolve2(rootDir);
9605
- const target = resolve2(absPath);
9773
+ const target = resolve2(absDir);
9606
9774
  const rel = relative2(root, target);
9607
- if (!rel || rel.startsWith("..")) return [];
9775
+ if (rel.startsWith("..")) return [];
9608
9776
  const found = [];
9609
- let cur = dirname(target);
9777
+ let cur = target;
9610
9778
  while (cur !== root) {
9611
9779
  const r = relative2(root, cur);
9612
9780
  if (!r || r.startsWith("..")) break;
@@ -9623,6 +9791,9 @@ function findSubdirMemoryAncestors(absPath, rootDir) {
9623
9791
  }
9624
9792
  return found;
9625
9793
  }
9794
+ function findSubdirMemoryAncestors(absPath, rootDir) {
9795
+ return findDirMemory(dirname(resolve2(absPath)), rootDir);
9796
+ }
9626
9797
  function readSubdirMemoryContent(path) {
9627
9798
  let raw;
9628
9799
  try {
@@ -9891,6 +10062,129 @@ function formatOutline(entries) {
9891
10062
  // src/tools/fs/search.ts
9892
10063
  import { promises as fs2 } from "fs";
9893
10064
  import * as pathMod3 from "path";
10065
+
10066
+ // src/tools/fs/regex-runner.ts
10067
+ import { Worker } from "worker_threads";
10068
+ var WORKER_SOURCE = `
10069
+ const { parentPort } = require("node:worker_threads");
10070
+ parentPort.on("message", (msg) => {
10071
+ const { id, text, source, flags } = msg;
10072
+ let re;
10073
+ try {
10074
+ re = new RegExp(source, flags);
10075
+ } catch (err) {
10076
+ parentPort.postMessage({ id, error: (err && err.message) ? err.message : String(err) });
10077
+ return;
10078
+ }
10079
+ const lines = text.split(/\\r?\\n/);
10080
+ const hits = [];
10081
+ for (let i = 0; i < lines.length; i++) {
10082
+ if (re.test(lines[i])) hits.push(i);
10083
+ }
10084
+ parentPort.postMessage({ id, hits });
10085
+ });
10086
+ `;
10087
+ var DEFAULT_TIMEOUT_MS = 6e4;
10088
+ var RegexRunner = class {
10089
+ worker = null;
10090
+ pending = /* @__PURE__ */ new Map();
10091
+ nextId = 1;
10092
+ defaultTimeoutMs;
10093
+ constructor(opts = {}) {
10094
+ this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
10095
+ }
10096
+ testLines(text, source, flags, opts = {}) {
10097
+ return new Promise((resolve5, reject) => {
10098
+ if (opts.signal?.aborted) {
10099
+ reject(new Error("regex evaluation aborted"));
10100
+ return;
10101
+ }
10102
+ if (!this.worker) this.worker = this.spawn();
10103
+ const id = this.nextId++;
10104
+ const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
10105
+ const timer = setTimeout(() => {
10106
+ this.pending.delete(id);
10107
+ this.killWorker();
10108
+ reject(new Error(`regex evaluation exceeded ${timeoutMs}ms`));
10109
+ }, timeoutMs);
10110
+ const entry = { resolve: resolve5, reject, timer };
10111
+ if (opts.signal) {
10112
+ entry.signal = opts.signal;
10113
+ entry.onAbort = () => {
10114
+ this.pending.delete(id);
10115
+ clearTimeout(timer);
10116
+ this.killWorker();
10117
+ reject(new Error("regex evaluation aborted"));
10118
+ };
10119
+ opts.signal.addEventListener("abort", entry.onAbort, { once: true });
10120
+ }
10121
+ this.pending.set(id, entry);
10122
+ this.worker.postMessage({ id, text, source, flags });
10123
+ });
10124
+ }
10125
+ async shutdown() {
10126
+ if (this.worker) {
10127
+ const w = this.worker;
10128
+ this.worker = null;
10129
+ await w.terminate();
10130
+ }
10131
+ for (const entry of this.pending.values()) {
10132
+ clearTimeout(entry.timer);
10133
+ if (entry.onAbort && entry.signal) {
10134
+ entry.signal.removeEventListener("abort", entry.onAbort);
10135
+ }
10136
+ entry.reject(new Error("regex runner shut down"));
10137
+ }
10138
+ this.pending.clear();
10139
+ }
10140
+ spawn() {
10141
+ const w = new Worker(WORKER_SOURCE, { eval: true });
10142
+ w.on("message", (msg) => {
10143
+ const entry = this.pending.get(msg.id);
10144
+ if (!entry) return;
10145
+ clearTimeout(entry.timer);
10146
+ if (entry.onAbort && entry.signal) {
10147
+ entry.signal.removeEventListener("abort", entry.onAbort);
10148
+ }
10149
+ this.pending.delete(msg.id);
10150
+ if (msg.error !== void 0) entry.reject(new Error(msg.error));
10151
+ else entry.resolve(msg.hits ?? []);
10152
+ });
10153
+ w.on("error", (err) => {
10154
+ if (this.worker !== w) return;
10155
+ this.failPending(err);
10156
+ });
10157
+ w.on("exit", () => {
10158
+ if (this.worker !== w) return;
10159
+ this.worker = null;
10160
+ if (this.pending.size > 0) this.failPending(new Error("regex worker exited"));
10161
+ });
10162
+ return w;
10163
+ }
10164
+ killWorker() {
10165
+ if (!this.worker) return;
10166
+ const w = this.worker;
10167
+ this.worker = null;
10168
+ void w.terminate();
10169
+ }
10170
+ failPending(err) {
10171
+ for (const entry of this.pending.values()) {
10172
+ clearTimeout(entry.timer);
10173
+ if (entry.onAbort && entry.signal) {
10174
+ entry.signal.removeEventListener("abort", entry.onAbort);
10175
+ }
10176
+ entry.reject(err);
10177
+ }
10178
+ this.pending.clear();
10179
+ }
10180
+ };
10181
+ var _runner = null;
10182
+ function getRegexRunner() {
10183
+ if (!_runner) _runner = new RegexRunner();
10184
+ return _runner;
10185
+ }
10186
+
10187
+ // src/tools/fs/search.ts
9894
10188
  function throwIfAborted(signal) {
9895
10189
  if (!signal?.aborted) return;
9896
10190
  throw new DOMException("search aborted by user", "AbortError");
@@ -9943,17 +10237,20 @@ async function searchFiles(ctx, startAbs, args) {
9943
10237
  }
9944
10238
  var MAX_HITS_PER_FILE = 30;
9945
10239
  var SUMMARY_MODE_TRIGGER_RATIO = 0.8;
10240
+ var WALK_DEADLINE_MS = 12e4;
9946
10241
  async function searchContent(ctx, startAbs, args) {
9947
10242
  throwIfAborted(args.signal);
9948
10243
  const caseSensitive = args.case_sensitive === true;
9949
10244
  const includeDeps = args.include_deps === true;
9950
10245
  const ctxLines = Math.max(0, Math.min(20, Math.floor(args.context ?? 0)));
9951
10246
  const summaryOnly = args.summary_only === true;
9952
- let re = null;
10247
+ const reFlags = caseSensitive ? "" : "i";
10248
+ let reSource = null;
9953
10249
  try {
9954
- re = new RegExp(args.pattern, caseSensitive ? "" : "i");
10250
+ new RegExp(args.pattern, reFlags);
10251
+ reSource = args.pattern;
9955
10252
  } catch {
9956
- re = null;
10253
+ reSource = null;
9957
10254
  }
9958
10255
  const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
9959
10256
  const matches = [];
@@ -9963,6 +10260,15 @@ async function searchContent(ctx, startAbs, args) {
9963
10260
  let summaryMode = summaryOnly;
9964
10261
  let summaryNoticeEmitted = false;
9965
10262
  const fileHitCounts = /* @__PURE__ */ new Map();
10263
+ const regexSkippedFiles = [];
10264
+ const t0 = Date.now();
10265
+ const throwIfTimedOut = () => {
10266
+ if (Date.now() - t0 > WALK_DEADLINE_MS) {
10267
+ throw new Error(
10268
+ `search_content exceeded ${WALK_DEADLINE_MS}ms \u2014 narrow the scope (path/glob) or simplify the pattern`
10269
+ );
10270
+ }
10271
+ };
9966
10272
  const pushLine = (out) => {
9967
10273
  if (totalBytes + out.length + 1 > ctx.maxListBytes) {
9968
10274
  matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
@@ -9997,6 +10303,7 @@ async function searchContent(ctx, startAbs, args) {
9997
10303
  for (const e of entries) {
9998
10304
  if (truncated) return;
9999
10305
  throwIfAborted(args.signal);
10306
+ throwIfTimedOut();
10000
10307
  if (e.isDirectory()) {
10001
10308
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
10002
10309
  await walk2(pathMod3.join(dir, e.name));
@@ -10033,13 +10340,25 @@ async function searchContent(ctx, startAbs, args) {
10033
10340
  const text = raw.toString("utf8");
10034
10341
  const rel = displayRel2(ctx.rootDir, full);
10035
10342
  const lines = text.split(/\r?\n/);
10036
- const hits = [];
10037
- for (let li = 0; li < lines.length; li++) {
10038
- throwIfAborted(args.signal);
10039
- const line = lines[li];
10040
- const lineForCheck = caseSensitive ? line : line.toLowerCase();
10041
- const hit = re ? re.test(line) : lineForCheck.includes(needle);
10042
- if (hit) hits.push(li);
10343
+ let hits;
10344
+ if (reSource !== null) {
10345
+ try {
10346
+ hits = await getRegexRunner().testLines(text, reSource, reFlags, {
10347
+ signal: args.signal
10348
+ });
10349
+ } catch (err) {
10350
+ const reason = err.message;
10351
+ if (reason.includes("aborted")) throw err;
10352
+ regexSkippedFiles.push({ rel, reason });
10353
+ continue;
10354
+ }
10355
+ } else {
10356
+ hits = [];
10357
+ for (let li = 0; li < lines.length; li++) {
10358
+ throwIfAborted(args.signal);
10359
+ const lineForCheck = caseSensitive ? lines[li] : lines[li].toLowerCase();
10360
+ if (lineForCheck.includes(needle)) hits.push(li);
10361
+ }
10043
10362
  }
10044
10363
  scanned++;
10045
10364
  if (hits.length === 0) continue;
@@ -10088,6 +10407,11 @@ async function searchContent(ctx, startAbs, args) {
10088
10407
  }
10089
10408
  };
10090
10409
  await walk2(startAbs);
10410
+ if (regexSkippedFiles.length > 0) {
10411
+ pushLine(
10412
+ `[regex timed out on ${regexSkippedFiles.length} file${regexSkippedFiles.length === 1 ? "" : "s"} \u2014 pattern may have catastrophic backtracking; first: ${regexSkippedFiles[0].rel}]`
10413
+ );
10414
+ }
10091
10415
  if (matches.length === 0) {
10092
10416
  return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
10093
10417
  }
@@ -10095,7 +10419,7 @@ async function searchContent(ctx, startAbs, args) {
10095
10419
  }
10096
10420
 
10097
10421
  // src/tools/filesystem.ts
10098
- var DEFAULT_OUTLINE_THRESHOLD_BYTES = 512 * 1024;
10422
+ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 64 * 1024;
10099
10423
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
10100
10424
  var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
10101
10425
  var OUTLINE_HEAD_LINES = 80;
@@ -10152,11 +10476,15 @@ function registerFilesystemTools(registry, opts) {
10152
10476
  const sessionApproved = /* @__PURE__ */ new Set();
10153
10477
  const shownSubdirMemory = /* @__PURE__ */ new Set();
10154
10478
  function withSubdirMemory(absPath, body) {
10155
- if (!memoryEnabled()) return body;
10156
- const ancestors = findSubdirMemoryAncestors(absPath, rootDir);
10157
- if (ancestors.length === 0) return body;
10479
+ return prependMemorySections(findSubdirMemoryAncestors(absPath, rootDir), body);
10480
+ }
10481
+ function withDirMemory(absDir, body) {
10482
+ return prependMemorySections(findDirMemory(absDir, rootDir), body);
10483
+ }
10484
+ function prependMemorySections(memPaths, body) {
10485
+ if (!memoryEnabled() || memPaths.length === 0) return body;
10158
10486
  const sections = [];
10159
- for (const memPath of [...ancestors].reverse()) {
10487
+ for (const memPath of [...memPaths].reverse()) {
10160
10488
  if (shownSubdirMemory.has(memPath)) continue;
10161
10489
  const content = readSubdirMemoryContent(memPath);
10162
10490
  if (!content) continue;
@@ -10233,11 +10561,7 @@ ${body}`;
10233
10561
  registry.register({
10234
10562
  name: "read_file",
10235
10563
  parallelSafe: true,
10236
- description: `Read a file under the sandbox root. Default behaviour returns FULL CONTENT for files at or under ${Math.round(DEFAULT_OUTLINE_THRESHOLD_BYTES / 1024)} KiB \u2014 trust the prompt cache, don't pre-truncate. Optional scoping:
10237
- - head: N \u2192 first N lines (cheap probe of imports / config head)
10238
- - tail: N \u2192 last N lines (recent-tail of a log)
10239
- - range: "A-B" \u2192 inclusive 1-indexed range (e.g. "120-180" around an edit site)
10240
- Files OVER the threshold auto-switch to outline mode: file metadata + first ${OUTLINE_HEAD_LINES} lines + a top-level symbol outline (TS/JS exports, Python def/class, Go func/type, Rust fn/struct/impl/trait, Markdown headings, Protobuf message/service/rpc, plain-text chapter markers) + concrete next-step commands. No middle bytes \u2014 drill in with range / search_content. Files over ${Math.round(HARD_MAX_FILE_BYTES / (1024 * 1024))} MiB are refused entirely (use grep / range). Binary files are refused \u2014 use get_file_info if you only need stat.`,
10564
+ description: `Read a file under the sandbox root. Default returns FULL CONTENT for files \u2264 ${Math.round(DEFAULT_OUTLINE_THRESHOLD_BYTES / 1024)} KiB. Optional scoping: head/tail (N lines), range "A-B" (1-indexed inclusive). Larger files auto-switch to outline mode (metadata + head + symbol outline for TS/JS/Python/Go/Rust/Markdown/Protobuf/text) \u2014 drill in with range or search_content. Files over ${Math.round(HARD_MAX_FILE_BYTES / (1024 * 1024))} MiB and binaries are refused \u2014 use get_file_info for stat.`,
10241
10565
  readOnly: true,
10242
10566
  stormExempt: true,
10243
10567
  parameters: {
@@ -10349,17 +10673,13 @@ ${slice.join("\n")}`);
10349
10673
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
10350
10674
  lines.push(e.isDirectory() ? `${e.name}/` : e.name);
10351
10675
  }
10352
- return lines.join("\n") || "(empty directory)";
10676
+ return withDirMemory(abs, lines.join("\n") || "(empty directory)");
10353
10677
  }
10354
10678
  });
10355
10679
  registry.register({
10356
10680
  name: "directory_tree",
10357
10681
  parallelSafe: true,
10358
- description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
10359
- - maxDepth defaults to 2 (root + one level). A depth-4 tree on a real repo blew ~5K tokens in one call. If you truly need deeper, pass maxDepth:N explicitly.
10360
- - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
10361
- - Large subtrees (>50 children) auto-collapse to "[N files, M dirs hidden \u2014 list_directory <path> to inspect]" so one huge folder can't dominate the output.
10362
- Prefer \`list_directory\` for a single-level view, \`search_files\` to find specific paths, and \`search_content\` to find code.`,
10682
+ description: `Recursively list entries with indented tree structure (dirs marked '/'). Budget-aware: maxDepth defaults to 2, large subtrees (>50 children) auto-collapse to "[N hidden \u2014 list_directory to inspect]", and ${[...SKIP_DIR_NAMES].sort().join(" / ")} are skipped unless include_deps:true. For single-level use list_directory; for path lookups use search_files; for code lookups use search_content.`,
10363
10683
  readOnly: true,
10364
10684
  parameters: {
10365
10685
  type: "object",
@@ -10460,38 +10780,38 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10460
10780
  registry.register({
10461
10781
  name: "search_content",
10462
10782
  parallelSafe: true,
10463
- description: "Recursively grep file CONTENTS for a substring or regex. This is the right tool for 'find all places that call X', 'where is Y referenced', 'what files contain Z'. Different from search_files (which matches FILE NAMES). Returns one match per line in 'path:line: text' format. Per-file hits are capped at 30 (a footer reports any extras); when the byte budget is mostly spent the remaining files switch to a 'rel: N matches' histogram so distribution stays visible instead of one popular file drowning the rest. Pass `summary_only:true` to skip line content entirely and get just the histogram. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) and binary files by default.",
10783
+ description: "Recursively grep file CONTENTS for a substring or regex \u2014 'where is X called', 'what files contain Y'. Returns one match per line as `path:line: text`. Per-file hit cap 30; when the byte budget is mostly spent, remaining files switch to a `rel: N matches` histogram. Pass `summary_only:true` for just the histogram. Skips dependency / VCS / build dirs and binary files. For file NAMES use search_files.",
10464
10784
  readOnly: true,
10465
10785
  parameters: {
10466
10786
  type: "object",
10467
10787
  properties: {
10468
10788
  pattern: {
10469
10789
  type: "string",
10470
- description: "Substring (or regex) to search file contents for."
10790
+ description: "Substring or regex."
10471
10791
  },
10472
10792
  path: {
10473
10793
  type: "string",
10474
- description: "Directory to start the search at (default: sandbox root)."
10794
+ description: "Search root (default: sandbox root)."
10475
10795
  },
10476
10796
  glob: {
10477
10797
  type: "string",
10478
- description: "Optional filename filter. Real glob when the value contains `*`, `?`, `{`, or `[` \u2014 e.g. '*.ts', '**/*.tsx', 'src/**/*.{ts,tsx}'. Plain substring otherwise \u2014 e.g. '.ts' (suffix), 'test' (anywhere in the name). Patterns containing `/` match against the path relative to the search root; otherwise just the basename."
10798
+ description: "Filename filter. Glob when it contains `*`/`?`/`{`/`[`; otherwise substring. Patterns with `/` match the path relative to the search root."
10479
10799
  },
10480
10800
  case_sensitive: {
10481
10801
  type: "boolean",
10482
- description: "When true, match case exactly. Default false (case-insensitive)."
10802
+ description: "Default false."
10483
10803
  },
10484
10804
  include_deps: {
10485
10805
  type: "boolean",
10486
- description: "When true, also search inside node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
10806
+ description: "Also search node_modules / .git / dist / build / etc. Default off."
10487
10807
  },
10488
10808
  context: {
10489
10809
  type: "integer",
10490
- description: "Lines of context to show around each match (both before and after). Default 0 (just the matching line). Capped at 20. Output uses ripgrep style: `:` after the line number on the matching line, `-` on context lines, `--` separating non-adjacent windows."
10810
+ description: "Lines of context around each match (both sides). Default 0, capped 20. Ripgrep-style output."
10491
10811
  },
10492
10812
  summary_only: {
10493
10813
  type: "boolean",
10494
- description: "When true, skip line content and return one 'rel: N matches' line per matching file. Use for 'where does this exist at all' questions before drilling in with a targeted read_file."
10814
+ description: "Skip line content, return `rel: N matches` per file. Use for 'where does this exist at all' before drilling in."
10495
10815
  }
10496
10816
  },
10497
10817
  required: ["pattern"]
@@ -10604,7 +10924,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10604
10924
  });
10605
10925
  registry.register({
10606
10926
  name: "multi_edit",
10607
- description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in a single atomic call. Edits run sequentially in array order; for edits that touch the same file, a later edit can match text inserted by an earlier one. If ANY edit fails (search not found, ambiguous match, empty search, file unreadable), NO files are written \u2014 atomic at the validation layer. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
10927
+ description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in one call. Edits validate across the full batch before writing. Validation failures leave all files untouched; disk write failures trigger best-effort rollback of files that may have been modified. Per-file edits run in array order, so a later edit can match text inserted by an earlier one. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
10608
10928
  parameters: {
10609
10929
  type: "object",
10610
10930
  properties: {
@@ -10746,19 +11066,33 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10746
11066
  }
10747
11067
 
10748
11068
  // src/tools/plan-core.ts
10749
- var SUBMIT_PLAN_DESCRIPTION = "Submit ONE concrete plan you've already decided on. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. CRITICAL: do NOT use submit_plan to present alternative routes (A/B/C, option 1/2/3) for the user to pick from \u2014 the picker only exposes approve/refine/cancel, so a menu plan strands the user with no way to choose. For branching decisions, call `ask_choice` instead; only call submit_plan once the user has picked a direction and you have a single actionable plan. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions. STRONGLY PREFERRED: pass `steps` \u2014 an array of {id, title, action, risk?} \u2014 so the UI renders a structured step list above the approval picker and tracks per-step progress. Use risk='high' for steps that touch prod data / break public APIs / are hard to undo; 'med' for non-trivial but reversible (multi-file edits, schema tweaks); 'low' for safe local work. After each step, call `mark_step_complete` so the user sees progress ticks.";
10750
- var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one step of the approved plan as done. MANDATORY: call this exactly once after finishing each step, before starting the next one \u2014 skipping it leaves the user staring at `0/N done` on the resume banner even when the work is finished, and they have no way to know which steps actually ran. The TUI updates the plan card's progress in place; the count is persisted to disk so it survives session resume. After the FINAL step, write a brief reply summarizing what was done and end the turn. Pass the `stepId` from the plan's steps array, a short `result` (what you did), and optional `notes` for anything surprising (errors, scope changes, follow-ups). This tool doesn't change any files. Don't call it if the plan didn't include structured steps, and don't invent ids that weren't in the original plan. If you only realized at the end that you skipped marking steps, mark them then \u2014 late is still better than never.";
10751
- var REVISE_PLAN_DESCRIPTION = "Surgically replace the REMAINING steps of an in-flight plan. Call this when the user has given feedback at a checkpoint that warrants a structured plan change \u2014 skip a step, swap two steps, add a new step, change risk, etc. Pass: `reason` (one sentence why), `remainingSteps` (the new tail of the plan, replacing whatever steps haven't been done yet), and optional `summary` (updated one-line plan summary). Done steps are NEVER touched \u2014 keep them out of `remainingSteps`. The TUI shows a diff (removed in red, kept in gray, added in green) and the user accepts or rejects. Don't call this for trivial mid-step adjustments \u2014 just keep executing. Don't call submit_plan for revisions either \u2014 that resets the whole plan including completed steps. Use submit_plan only when the entire approach has changed; use revise_plan when the tail needs editing.";
11069
+ var SUBMIT_PLAN_DESCRIPTION = "Submit ONE concrete plan for review. The user approves / refines / cancels \u2014 write a markdown plan body and (strongly preferred) a structured `steps` array. Use for multi-file refactors, architecture changes, anything expensive to undo. Skip for small fixes. Do NOT use for A/B/C menus \u2014 the picker has no branch selector, so a menu plan strands the user; call `ask_choice` for branching decisions. See the system prompt for fuller guidance.";
11070
+ var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one approved-plan step as done. Call exactly once after finishing each step, before starting the next. After the FINAL step, write a brief reply summarizing what was done and end the turn. Skip if the plan didn't include structured steps.";
11071
+ var REVISE_PLAN_DESCRIPTION = "Replace the REMAINING steps of an in-flight plan when checkpoint feedback warrants a structural change. Pass `reason`, the new `remainingSteps` tail (done steps are untouched \u2014 keep them out), and optional updated `summary`. Don't call submit_plan for revisions \u2014 it resets the whole plan.";
10752
11072
  var STEP_ITEM_SCHEMA = {
10753
11073
  type: "object",
10754
11074
  properties: {
10755
11075
  id: { type: "string", description: "Stable id, e.g. step-1." },
10756
11076
  title: { type: "string", description: "Short imperative title." },
10757
- action: { type: "string", description: "One-sentence description of the concrete action." },
11077
+ action: { type: "string", description: "One-sentence concrete action." },
10758
11078
  risk: {
10759
11079
  type: "string",
10760
11080
  enum: ["low", "med", "high"],
10761
- description: "Self-assessed risk. 'high' = hard-to-undo / touches prod / breaks API; 'med' = non-trivial but reversible; 'low' = safe local work. The UI shows a colored dot per step so the user knows where to focus review. Omit if you're unsure."
11081
+ description: "high = hard-to-undo / prod / API break; med = reversible multi-file; low = safe local. Omit if unsure."
11082
+ },
11083
+ targets: {
11084
+ type: "array",
11085
+ description: "Optional. Files/dirs/modules this step touches.",
11086
+ items: { type: "string" }
11087
+ },
11088
+ acceptance: {
11089
+ type: "string",
11090
+ description: "Optional. One-sentence completion criterion."
11091
+ },
11092
+ verification: {
11093
+ type: "array",
11094
+ description: "Optional. Verification commands/checks for this step.",
11095
+ items: { type: "string" }
10762
11096
  }
10763
11097
  },
10764
11098
  required: ["id", "title", "action"]
@@ -10780,10 +11114,42 @@ function sanitizeSteps(raw) {
10780
11114
  const step = { id, title, action };
10781
11115
  const risk = sanitizeRisk(e.risk);
10782
11116
  if (risk) step.risk = risk;
11117
+ const targets = sanitizeStringList(e.targets);
11118
+ if (targets) step.targets = targets;
11119
+ const acceptance = typeof e.acceptance === "string" ? e.acceptance.trim() : "";
11120
+ if (acceptance) step.acceptance = acceptance;
11121
+ const verification = sanitizeStringList(e.verification);
11122
+ if (verification) step.verification = verification;
10783
11123
  steps.push(step);
10784
11124
  }
10785
11125
  return steps.length > 0 ? steps : void 0;
10786
11126
  }
11127
+ function sanitizeStringList(raw) {
11128
+ if (!Array.isArray(raw)) return void 0;
11129
+ const out = raw.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
11130
+ return out.length > 0 ? out : void 0;
11131
+ }
11132
+ function sanitizeEvidence(raw) {
11133
+ if (!Array.isArray(raw)) return void 0;
11134
+ const out = [];
11135
+ for (const item of raw) {
11136
+ if (!item || typeof item !== "object") continue;
11137
+ const e = item;
11138
+ const kind = e.kind;
11139
+ if (kind !== "verification" && kind !== "diff" && kind !== "checkpoint" && kind !== "manual") {
11140
+ continue;
11141
+ }
11142
+ const summary = typeof e.summary === "string" ? e.summary.trim() : "";
11143
+ if (!summary) continue;
11144
+ const evidence = { kind, summary };
11145
+ const command = typeof e.command === "string" ? e.command.trim() : "";
11146
+ if (command) evidence.command = command;
11147
+ const paths = sanitizeStringList(e.paths);
11148
+ if (paths) evidence.paths = paths;
11149
+ out.push(evidence);
11150
+ }
11151
+ return out.length > 0 ? out : void 0;
11152
+ }
10787
11153
  function registerSubmitPlan(registry, opts) {
10788
11154
  registry.register({
10789
11155
  name: "submit_plan",
@@ -10794,16 +11160,16 @@ function registerSubmitPlan(registry, opts) {
10794
11160
  properties: {
10795
11161
  plan: {
10796
11162
  type: "string",
10797
- description: "Markdown-formatted plan. Lead with a one-sentence summary. Then a file-by-file breakdown of what you'll change and why. Flag any risks or open questions at the end so the user can weigh in before you start."
11163
+ description: "Markdown plan: one-line summary, file-by-file breakdown, risks/open questions."
10798
11164
  },
10799
11165
  steps: {
10800
11166
  type: "array",
10801
- description: "Structured step list (strongly recommended). When provided, the UI renders a compact step list above the approval picker AND tracks per-step progress via `mark_step_complete`. Use stable ids (step-1, step-2, ...). Skip only for tiny one-step plans where the markdown body is enough.",
11167
+ description: "Structured step list \u2014 strongly recommended for >1 step. Stable ids (step-1, step-2, ...).",
10802
11168
  items: STEP_ITEM_SCHEMA
10803
11169
  },
10804
11170
  summary: {
10805
11171
  type: "string",
10806
- description: "Optional. One-sentence human-friendly title for the plan, ~80 chars max. Surfaces in the PlanConfirm picker header and in /plans listings ('\u25B8 refactor auth into signed tokens \xB7 2/5 done'). Skip for trivial plans where the first line of the markdown body is already short and clear."
11172
+ description: "Optional ~80-char plan title for the picker header and /plans listings."
10807
11173
  }
10808
11174
  },
10809
11175
  required: ["plan"]
@@ -10841,19 +11207,33 @@ function registerMarkStepComplete(registry, opts) {
10841
11207
  properties: {
10842
11208
  stepId: {
10843
11209
  type: "string",
10844
- description: "The id of the step being marked complete. Must match one from submit_plan's steps array."
11210
+ description: "Step id from submit_plan's steps array."
10845
11211
  },
10846
11212
  title: {
10847
11213
  type: "string",
10848
- description: "Optional. The step's title, echoed back for the UI. If omitted, the UI falls back to the id."
11214
+ description: "Optional. Echoed for the UI; falls back to id."
10849
11215
  },
10850
11216
  result: {
10851
11217
  type: "string",
10852
- description: "One-sentence summary of what was done for this step."
11218
+ description: "One-sentence summary of what was done."
10853
11219
  },
10854
11220
  notes: {
10855
11221
  type: "string",
10856
- description: "Optional. Anything surprising \u2014 blockers hit, assumptions revised, follow-ups for later steps."
11222
+ description: "Optional. Surprises \u2014 blockers, revised assumptions, follow-ups."
11223
+ },
11224
+ evidence: {
11225
+ type: "array",
11226
+ description: "Optional. Verification summary / diff / checkpoint ref / manual note.",
11227
+ items: {
11228
+ type: "object",
11229
+ properties: {
11230
+ kind: { type: "string", enum: ["verification", "diff", "checkpoint", "manual"] },
11231
+ summary: { type: "string" },
11232
+ command: { type: "string" },
11233
+ paths: { type: "array", items: { type: "string" } }
11234
+ },
11235
+ required: ["kind", "summary"]
11236
+ }
10857
11237
  }
10858
11238
  },
10859
11239
  required: ["stepId", "result"]
@@ -10871,9 +11251,15 @@ function registerMarkStepComplete(registry, opts) {
10871
11251
  }
10872
11252
  const title = typeof args?.title === "string" ? args.title.trim() || void 0 : void 0;
10873
11253
  const notes = typeof args?.notes === "string" ? args.notes.trim() || void 0 : void 0;
11254
+ const evidence = sanitizeEvidence(args?.evidence);
11255
+ const evidenceReason = opts.requireStepEvidence?.({ stepId, title });
11256
+ if (evidenceReason && (!evidence || evidence.length === 0)) {
11257
+ throw new Error(`mark_step_complete: evidence required \u2014 ${evidenceReason}`);
11258
+ }
10874
11259
  const update = { kind: "step_completed", stepId, result };
10875
11260
  if (title) update.title = title;
10876
11261
  if (notes) update.notes = notes;
11262
+ if (evidence) update.evidence = evidence;
10877
11263
  opts.onStepCompleted?.(update);
10878
11264
  const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
10879
11265
  kind: "plan_checkpoint",
@@ -10898,16 +11284,16 @@ function registerRevisePlan(registry, opts) {
10898
11284
  properties: {
10899
11285
  reason: {
10900
11286
  type: "string",
10901
- description: "One sentence explaining why you're revising \u2014 what the user asked for, what changed your assessment."
11287
+ description: "One sentence \u2014 why you're revising / what the user asked for."
10902
11288
  },
10903
11289
  remainingSteps: {
10904
11290
  type: "array",
10905
- description: "The new tail of the plan \u2014 what should run from here on. Each entry: {id, title, action, risk?}. Use stable ids; reuse old ids when a step is just being adjusted, generate new ones for genuinely new steps.",
11291
+ description: "New tail of the plan. Reuse old ids when adjusting; new ids for new steps.",
10906
11292
  items: STEP_ITEM_SCHEMA
10907
11293
  },
10908
11294
  summary: {
10909
11295
  type: "string",
10910
- description: "Optional. Updated one-line plan summary if the overall framing has shifted."
11296
+ description: "Optional. Updated one-line summary when framing has shifted."
10911
11297
  }
10912
11298
  },
10913
11299
  required: ["reason", "remainingSteps"]
@@ -10945,7 +11331,7 @@ function registerPlanTool(registry, opts = {}) {
10945
11331
  }
10946
11332
 
10947
11333
  // src/tools/todo.ts
10948
- var DESCRIPTION = 'In-session task tracker for multi-step work. NOT a plan \u2014 no approval gate, no checkpoint pauses, doesn\'t touch any files. The tool replaces the entire todo list every call (set semantics, NOT append). Pass the FULL list every time.\n\nWhen to use:\n\u2022 The task has 3+ distinct steps and you want to keep them straight as you work.\n\u2022 The user gave you a multi-part request ("do A, then B, then C").\n\u2022 You\'re partway through a long task and want to record where you are so a future you doesn\'t lose the thread.\n\nWhen NOT to use:\n\u2022 One-shot edits, single-question answers, single-tool tasks.\n\u2022 User-facing approval gates \u2192 that\'s `submit_plan`.\n\u2022 Branching choices \u2192 that\'s `ask_choice`.\n\nRules:\n\u2022 Exactly ONE todo may have status:"in_progress" at a time (or zero \u2014 between steps).\n\u2022 Mark a todo "completed" the moment it\'s actually done \u2014 don\'t batch.\n\u2022 Each todo: `content` (imperative, e.g. "Add tests"), `activeForm` (gerund shown while running, e.g. "Adding tests"), `status`.\n\u2022 Empty `todos:[]` is allowed \u2014 it clears the list when work is fully done.';
11334
+ var DESCRIPTION = "In-session task tracker for 3+ step work. NOT a plan \u2014 no approval gate, no checkpoint, no files touched. Each call REPLACES the entire list (set semantics) \u2014 pass the FULL list. Exactly one item may be in_progress at a time; flip to completed the moment that step's done. Pass `[]` to clear. For approval gates use submit_plan; for branching choices use ask_choice.";
10949
11335
  function validateTodos(raw) {
10950
11336
  if (!Array.isArray(raw)) {
10951
11337
  throw new Error("todo_write: `todos` must be an array");
@@ -11571,4 +11957,4 @@ export {
11571
11957
  he/he.js:
11572
11958
  (*! https://mths.be/he v1.2.0 by @mathias | MIT license *)
11573
11959
  */
11574
- //# sourceMappingURL=chunk-ZOQHVQON.js.map
11960
+ //# sourceMappingURL=chunk-JBH5RM7X.js.map