reasonix 0.47.0 → 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 (108) hide show
  1. package/dist/cli/{acp-QK3DMC53.js → acp-GEOAKSTU.js} +21 -49
  2. package/dist/cli/acp-GEOAKSTU.js.map +1 -0
  3. package/dist/cli/{chat-VV5UWY4V.js → chat-YTPATMMG.js} +23 -23
  4. package/dist/cli/{chunk-FDKOUJKZ.js → chunk-2XY77LW7.js} +7 -7
  5. package/dist/cli/{chunk-QVDWH2A2.js → chunk-4MFCAZ2W.js} +3 -3
  6. package/dist/cli/{chunk-24A7FHGJ.js → chunk-6CRPCJAU.js} +14 -1
  7. package/dist/cli/chunk-6CRPCJAU.js.map +1 -0
  8. package/dist/cli/{chunk-VKYSZKH2.js → chunk-6QC5RQLE.js} +2 -2
  9. package/dist/cli/chunk-BQ6HC66J.js +530 -0
  10. package/dist/cli/chunk-BQ6HC66J.js.map +1 -0
  11. package/dist/cli/{chunk-OJVITDGB.js → chunk-CCJAP7G3.js} +2 -2
  12. package/dist/cli/{chunk-R6GQKKBW.js → chunk-CNG32VAB.js} +2 -2
  13. package/dist/cli/{chunk-QVUFWDD2.js → chunk-DN4B5S6Y.js} +2 -2
  14. package/dist/cli/{chunk-LBLR4CUZ.js → chunk-DQ6K5ZQ7.js} +2 -2
  15. package/dist/cli/{chunk-VNQGCA3Q.js → chunk-DWPAKZTY.js} +14 -3
  16. package/dist/cli/chunk-DWPAKZTY.js.map +1 -0
  17. package/dist/cli/{chunk-BWYVFFKR.js → chunk-GH7DC2Y5.js} +2 -2
  18. package/dist/cli/{chunk-BYYVYJDX.js → chunk-HUILPCYX.js} +3 -3
  19. package/dist/cli/{chunk-ICAFSZHS.js → chunk-JBH5RM7X.js} +174 -65
  20. package/dist/cli/chunk-JBH5RM7X.js.map +1 -0
  21. package/dist/cli/{chunk-K6GUKSXH.js → chunk-KVZZ5U75.js} +2 -2
  22. package/dist/cli/{chunk-WF7TPVZM.js → chunk-KYQVQ5X4.js} +84 -9
  23. package/dist/cli/chunk-KYQVQ5X4.js.map +1 -0
  24. package/dist/cli/{chunk-KDRUEXII.js → chunk-NRQ5UP5T.js} +20 -6
  25. package/dist/cli/chunk-NRQ5UP5T.js.map +1 -0
  26. package/dist/cli/{chunk-VJMBISEI.js → chunk-QCFLPSPH.js} +2 -2
  27. package/dist/cli/{chunk-YDPLF7XR.js → chunk-T5A7EY6B.js} +2 -2
  28. package/dist/cli/{chunk-VMUUFWFF.js → chunk-TDHXB2ER.js} +2 -2
  29. package/dist/cli/{chunk-GDKB2PPK.js → chunk-TRSAHHCL.js} +107 -11
  30. package/dist/cli/chunk-TRSAHHCL.js.map +1 -0
  31. package/dist/cli/{chunk-6J6BSUCR.js → chunk-TRWHTFG7.js} +2 -2
  32. package/dist/cli/{chunk-VC2CQA5D.js → chunk-XD6P7AFH.js} +26 -29
  33. package/dist/cli/chunk-XD6P7AFH.js.map +1 -0
  34. package/dist/cli/{chunk-ICSYGIPN.js → chunk-XMHP7BEE.js} +421 -80
  35. package/dist/cli/chunk-XMHP7BEE.js.map +1 -0
  36. package/dist/cli/{chunk-COWPEX54.js → chunk-YFP3MYMY.js} +5 -5
  37. package/dist/cli/{chunk-CI2PF5QX.js → chunk-ZXSCAODE.js} +8 -8
  38. package/dist/cli/{chunk-CI2PF5QX.js.map → chunk-ZXSCAODE.js.map} +1 -1
  39. package/dist/cli/{code-C24TUAE5.js → code-Q4NRVEDG.js} +29 -27
  40. package/dist/cli/code-Q4NRVEDG.js.map +1 -0
  41. package/dist/cli/{commands-RR3GIYOK.js → commands-4CDI4GFM.js} +4 -4
  42. package/dist/cli/{commit-FSHPIINM.js → commit-GW7LDQP5.js} +3 -3
  43. package/dist/cli/{desktop-7NCHPEFB.js → desktop-EG6P5SF2.js} +80 -22
  44. package/dist/cli/desktop-EG6P5SF2.js.map +1 -0
  45. package/dist/cli/{diff-RAAHHLHV.js → diff-VI2YX4FN.js} +8 -8
  46. package/dist/cli/{doctor-PKVQIXRT.js → doctor-CQTTZP27.js} +8 -8
  47. package/dist/cli/index.js +45 -37
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/cli/{mcp-CRJ26PP4.js → mcp-J2UCD4RZ.js} +2 -2
  50. package/dist/cli/{mcp-browse-QPAOWZOP.js → mcp-browse-GSX34JEK.js} +2 -2
  51. package/dist/cli/{mcp-inspect-CVCLABRS.js → mcp-inspect-RRFYF4ZV.js} +2 -2
  52. package/dist/cli/{prompt-SKYXERSI.js → prompt-5TQPIVHV.js} +3 -3
  53. package/dist/cli/{replay-KPDW2ZMJ.js → replay-MJCEMODU.js} +8 -8
  54. package/dist/cli/{run-WIKDIXTG.js → run-P4D5VDYE.js} +13 -13
  55. package/dist/cli/{server-P6V2G3P6.js → server-C25JNNZV.js} +11 -11
  56. package/dist/cli/{sessions-2NULRMSA.js → sessions-QIONZJQ6.js} +12 -12
  57. package/dist/cli/{setup-Y5WDBQFL.js → setup-NLQ6G5G4.js} +6 -6
  58. package/dist/cli/setup-NLQ6G5G4.js.map +1 -0
  59. package/dist/cli/{stats-T7BL2YOR.js → stats-DFZEXHP4.js} +6 -6
  60. package/dist/cli/{version-3KWDNWLN.js → version-GR3X3MPI.js} +12 -12
  61. package/dist/index.d.ts +40 -48
  62. package/dist/index.js +286 -237
  63. package/dist/index.js.map +1 -1
  64. package/package.json +3 -1
  65. package/dist/cli/acp-QK3DMC53.js.map +0 -1
  66. package/dist/cli/chunk-24A7FHGJ.js.map +0 -1
  67. package/dist/cli/chunk-GDKB2PPK.js.map +0 -1
  68. package/dist/cli/chunk-ICAFSZHS.js.map +0 -1
  69. package/dist/cli/chunk-ICSYGIPN.js.map +0 -1
  70. package/dist/cli/chunk-KDRUEXII.js.map +0 -1
  71. package/dist/cli/chunk-UDVFBEXC.js +0 -642
  72. package/dist/cli/chunk-UDVFBEXC.js.map +0 -1
  73. package/dist/cli/chunk-VC2CQA5D.js.map +0 -1
  74. package/dist/cli/chunk-VNQGCA3Q.js.map +0 -1
  75. package/dist/cli/chunk-WF7TPVZM.js.map +0 -1
  76. package/dist/cli/code-C24TUAE5.js.map +0 -1
  77. package/dist/cli/desktop-7NCHPEFB.js.map +0 -1
  78. package/dist/cli/setup-Y5WDBQFL.js.map +0 -1
  79. /package/dist/cli/{chat-VV5UWY4V.js.map → chat-YTPATMMG.js.map} +0 -0
  80. /package/dist/cli/{chunk-FDKOUJKZ.js.map → chunk-2XY77LW7.js.map} +0 -0
  81. /package/dist/cli/{chunk-QVDWH2A2.js.map → chunk-4MFCAZ2W.js.map} +0 -0
  82. /package/dist/cli/{chunk-VKYSZKH2.js.map → chunk-6QC5RQLE.js.map} +0 -0
  83. /package/dist/cli/{chunk-OJVITDGB.js.map → chunk-CCJAP7G3.js.map} +0 -0
  84. /package/dist/cli/{chunk-R6GQKKBW.js.map → chunk-CNG32VAB.js.map} +0 -0
  85. /package/dist/cli/{chunk-QVUFWDD2.js.map → chunk-DN4B5S6Y.js.map} +0 -0
  86. /package/dist/cli/{chunk-LBLR4CUZ.js.map → chunk-DQ6K5ZQ7.js.map} +0 -0
  87. /package/dist/cli/{chunk-BWYVFFKR.js.map → chunk-GH7DC2Y5.js.map} +0 -0
  88. /package/dist/cli/{chunk-BYYVYJDX.js.map → chunk-HUILPCYX.js.map} +0 -0
  89. /package/dist/cli/{chunk-K6GUKSXH.js.map → chunk-KVZZ5U75.js.map} +0 -0
  90. /package/dist/cli/{chunk-VJMBISEI.js.map → chunk-QCFLPSPH.js.map} +0 -0
  91. /package/dist/cli/{chunk-YDPLF7XR.js.map → chunk-T5A7EY6B.js.map} +0 -0
  92. /package/dist/cli/{chunk-VMUUFWFF.js.map → chunk-TDHXB2ER.js.map} +0 -0
  93. /package/dist/cli/{chunk-6J6BSUCR.js.map → chunk-TRWHTFG7.js.map} +0 -0
  94. /package/dist/cli/{chunk-COWPEX54.js.map → chunk-YFP3MYMY.js.map} +0 -0
  95. /package/dist/cli/{commands-RR3GIYOK.js.map → commands-4CDI4GFM.js.map} +0 -0
  96. /package/dist/cli/{commit-FSHPIINM.js.map → commit-GW7LDQP5.js.map} +0 -0
  97. /package/dist/cli/{diff-RAAHHLHV.js.map → diff-VI2YX4FN.js.map} +0 -0
  98. /package/dist/cli/{doctor-PKVQIXRT.js.map → doctor-CQTTZP27.js.map} +0 -0
  99. /package/dist/cli/{mcp-CRJ26PP4.js.map → mcp-J2UCD4RZ.js.map} +0 -0
  100. /package/dist/cli/{mcp-browse-QPAOWZOP.js.map → mcp-browse-GSX34JEK.js.map} +0 -0
  101. /package/dist/cli/{mcp-inspect-CVCLABRS.js.map → mcp-inspect-RRFYF4ZV.js.map} +0 -0
  102. /package/dist/cli/{prompt-SKYXERSI.js.map → prompt-5TQPIVHV.js.map} +0 -0
  103. /package/dist/cli/{replay-KPDW2ZMJ.js.map → replay-MJCEMODU.js.map} +0 -0
  104. /package/dist/cli/{run-WIKDIXTG.js.map → run-P4D5VDYE.js.map} +0 -0
  105. /package/dist/cli/{server-P6V2G3P6.js.map → server-C25JNNZV.js.map} +0 -0
  106. /package/dist/cli/{sessions-2NULRMSA.js.map → sessions-QIONZJQ6.js.map} +0 -0
  107. /package/dist/cli/{stats-T7BL2YOR.js.map → stats-DFZEXHP4.js.map} +0 -0
  108. /package/dist/cli/{version-3KWDNWLN.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-UDVFBEXC.js";
6
+ } from "./chunk-BQ6HC66J.js";
7
7
  import {
8
8
  countTokens,
9
9
  countTokensBounded,
@@ -12,12 +12,12 @@ import {
12
12
  } from "./chunk-6OWJV3YW.js";
13
13
  import {
14
14
  Usage
15
- } from "./chunk-VNQGCA3Q.js";
15
+ } from "./chunk-DWPAKZTY.js";
16
16
  import {
17
17
  applyEdit,
18
18
  applyMultiEdit,
19
19
  pauseGate
20
- } from "./chunk-GDKB2PPK.js";
20
+ } from "./chunk-TRSAHHCL.js";
21
21
  import {
22
22
  NEGATIVE_CLAIM_RULE,
23
23
  PROJECT_MEMORY_FILES,
@@ -28,7 +28,7 @@ import {
28
28
  import {
29
29
  formatHookOutcomeMessage,
30
30
  runHooks
31
- } from "./chunk-BWYVFFKR.js";
31
+ } from "./chunk-GH7DC2Y5.js";
32
32
  import {
33
33
  ignoredByLayers,
34
34
  loadGitignoreAt,
@@ -46,10 +46,10 @@ import {
46
46
  DEEPSEEK_CONTEXT_TOKENS,
47
47
  DEFAULT_CONTEXT_TOKENS,
48
48
  SessionStats
49
- } from "./chunk-VJMBISEI.js";
49
+ } from "./chunk-QCFLPSPH.js";
50
50
  import {
51
51
  t
52
- } from "./chunk-KDRUEXII.js";
52
+ } from "./chunk-NRQ5UP5T.js";
53
53
  import {
54
54
  DEFAULT_INDEX_EXCLUDES,
55
55
  addProjectPathAllowed,
@@ -60,7 +60,7 @@ import {
60
60
  require_picomatch,
61
61
  webSearchEndpoint,
62
62
  webSearchEngine
63
- } from "./chunk-24A7FHGJ.js";
63
+ } from "./chunk-6CRPCJAU.js";
64
64
  import {
65
65
  __commonJS,
66
66
  __esm,
@@ -6309,10 +6309,13 @@ var ToolRegistry = class {
6309
6309
  _autoFlatten;
6310
6310
  _planMode = false;
6311
6311
  _interceptor = null;
6312
+ _interceptors = [];
6312
6313
  _auditListener = null;
6313
6314
  _resultAugmenter = null;
6314
6315
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
6315
6316
  _lastMalformed = /* @__PURE__ */ new Map();
6317
+ /** Per-tool fingerprint of the last host-side interceptor rejection. */
6318
+ _lastInterceptorRejection = /* @__PURE__ */ new Map();
6316
6319
  constructor(opts = {}) {
6317
6320
  this._autoFlatten = opts.autoFlatten !== false;
6318
6321
  }
@@ -6328,6 +6331,18 @@ var ToolRegistry = class {
6328
6331
  setToolInterceptor(fn) {
6329
6332
  this._interceptor = fn;
6330
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
+ }
6331
6346
  setAuditListener(fn) {
6332
6347
  this._auditListener = fn;
6333
6348
  }
@@ -6416,16 +6431,21 @@ var ToolRegistry = class {
6416
6431
  rejectedReason: "plan-mode"
6417
6432
  });
6418
6433
  }
6419
- 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) {
6420
6436
  try {
6421
- const short = await this._interceptor(name, args);
6422
- 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
+ }
6423
6442
  } catch (err) {
6424
6443
  return JSON.stringify({
6425
6444
  error: `${name}: interceptor failed \u2014 ${err.message}`
6426
6445
  });
6427
6446
  }
6428
6447
  }
6448
+ this._lastInterceptorRejection.delete(name);
6429
6449
  if (opts.signal?.aborted) {
6430
6450
  return JSON.stringify({
6431
6451
  error: `${name}: aborted before dispatch (user interrupt)`,
@@ -6463,13 +6483,16 @@ var ToolRegistry = class {
6463
6483
  finalResult = JSON.stringify({ error: `${e.name}: ${e.message}` });
6464
6484
  }
6465
6485
  }
6486
+ return this._augmentResult(name, args, finalResult);
6487
+ }
6488
+ _augmentResult(name, args, result) {
6466
6489
  if (this._resultAugmenter) {
6467
6490
  try {
6468
- return this._resultAugmenter(name, args, finalResult);
6491
+ return this._resultAugmenter(name, args, result);
6469
6492
  } catch {
6470
6493
  }
6471
6494
  }
6472
- return finalResult;
6495
+ return result;
6473
6496
  }
6474
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. */
6475
6498
  _noteMalformed(name, fingerprint, detail) {
@@ -6483,7 +6506,35 @@ var ToolRegistry = class {
6483
6506
  }
6484
6507
  return JSON.stringify({ error: `${name}: ${detail}` });
6485
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
+ }
6486
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
+ }
6487
6538
  function isReadOnlyCall(tool, args) {
6488
6539
  if (tool.readOnlyCheck) {
6489
6540
  try {
@@ -8998,7 +9049,7 @@ function registerMemoryTools(registry, opts = {}) {
8998
9049
  }
8999
9050
  registry.register({
9000
9051
  name: "remember",
9001
- 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.",
9002
9053
  parameters: {
9003
9054
  type: "object",
9004
9055
  properties: {
@@ -9009,29 +9060,29 @@ function registerMemoryTools(registry, opts = {}) {
9009
9060
  scope: {
9010
9061
  type: "string",
9011
9062
  enum: ["global", "project"],
9012
- 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`)."
9013
9064
  },
9014
9065
  name: {
9015
9066
  type: "string",
9016
- 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)."
9017
9068
  },
9018
9069
  description: {
9019
9070
  type: "string",
9020
- description: "One-line summary shown in MEMORY.md (under ~150 chars)."
9071
+ description: "\u2264150 char one-liner shown in MEMORY.md."
9021
9072
  },
9022
9073
  content: {
9023
9074
  type: "string",
9024
- 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:**."
9025
9076
  },
9026
9077
  priority: {
9027
9078
  type: "string",
9028
9079
  enum: ["low", "medium", "high"],
9029
- 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."
9030
9081
  },
9031
9082
  expires: {
9032
9083
  type: "string",
9033
9084
  enum: ["project_end"],
9034
- 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."
9035
9086
  }
9036
9087
  },
9037
9088
  required: ["type", "scope", "name", "description", "content"]
@@ -9148,26 +9199,26 @@ function sanitizeOptions(raw) {
9148
9199
  function registerChoiceTool(registry, opts = {}) {
9149
9200
  registry.register({
9150
9201
  name: "ask_choice",
9151
- 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.",
9152
9203
  readOnly: true,
9153
9204
  parameters: {
9154
9205
  type: "object",
9155
9206
  properties: {
9156
9207
  question: {
9157
9208
  type: "string",
9158
- 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."
9159
9210
  },
9160
9211
  options: {
9161
9212
  type: "array",
9162
- 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.",
9163
9214
  items: {
9164
9215
  type: "object",
9165
9216
  properties: {
9166
- id: { type: "string", description: "Short stable id (A, B, C, or option-1)." },
9167
- 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." },
9168
9219
  summary: {
9169
9220
  type: "string",
9170
- description: "Optional. A second dimmed line with more detail. Keep under ~80 chars."
9221
+ description: "Optional dimmed second line, \u226480 chars."
9171
9222
  }
9172
9223
  },
9173
9224
  required: ["id", "title"]
@@ -9175,7 +9226,7 @@ function registerChoiceTool(registry, opts = {}) {
9175
9226
  },
9176
9227
  allowCustom: {
9177
9228
  type: "boolean",
9178
- 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."
9179
9230
  }
9180
9231
  },
9181
9232
  required: ["question", "options"]
@@ -9660,8 +9711,8 @@ function registerWebTools(registry, opts = {}) {
9660
9711
  required: ["query"]
9661
9712
  },
9662
9713
  fn: async (args, ctx) => {
9663
- const engine = opts.webSearchEngine ?? webSearchEngine();
9664
- const endpoint = opts.webSearchEndpoint ?? webSearchEndpoint();
9714
+ const engine = webSearchEngine();
9715
+ const endpoint = webSearchEndpoint();
9665
9716
  const results = await webSearch(args.query, {
9666
9717
  topK: args.topK ?? defaultTopK,
9667
9718
  signal: ctx?.signal,
@@ -10368,7 +10419,7 @@ async function searchContent(ctx, startAbs, args) {
10368
10419
  }
10369
10420
 
10370
10421
  // src/tools/filesystem.ts
10371
- var DEFAULT_OUTLINE_THRESHOLD_BYTES = 512 * 1024;
10422
+ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 64 * 1024;
10372
10423
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
10373
10424
  var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
10374
10425
  var OUTLINE_HEAD_LINES = 80;
@@ -10510,11 +10561,7 @@ ${body}`;
10510
10561
  registry.register({
10511
10562
  name: "read_file",
10512
10563
  parallelSafe: true,
10513
- 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:
10514
- - head: N \u2192 first N lines (cheap probe of imports / config head)
10515
- - tail: N \u2192 last N lines (recent-tail of a log)
10516
- - range: "A-B" \u2192 inclusive 1-indexed range (e.g. "120-180" around an edit site)
10517
- 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.`,
10518
10565
  readOnly: true,
10519
10566
  stormExempt: true,
10520
10567
  parameters: {
@@ -10632,11 +10679,7 @@ ${slice.join("\n")}`);
10632
10679
  registry.register({
10633
10680
  name: "directory_tree",
10634
10681
  parallelSafe: true,
10635
- description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
10636
- - 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.
10637
- - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
10638
- - 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.
10639
- 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.`,
10640
10683
  readOnly: true,
10641
10684
  parameters: {
10642
10685
  type: "object",
@@ -10737,38 +10780,38 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10737
10780
  registry.register({
10738
10781
  name: "search_content",
10739
10782
  parallelSafe: true,
10740
- 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.",
10741
10784
  readOnly: true,
10742
10785
  parameters: {
10743
10786
  type: "object",
10744
10787
  properties: {
10745
10788
  pattern: {
10746
10789
  type: "string",
10747
- description: "Substring (or regex) to search file contents for."
10790
+ description: "Substring or regex."
10748
10791
  },
10749
10792
  path: {
10750
10793
  type: "string",
10751
- description: "Directory to start the search at (default: sandbox root)."
10794
+ description: "Search root (default: sandbox root)."
10752
10795
  },
10753
10796
  glob: {
10754
10797
  type: "string",
10755
- 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."
10756
10799
  },
10757
10800
  case_sensitive: {
10758
10801
  type: "boolean",
10759
- description: "When true, match case exactly. Default false (case-insensitive)."
10802
+ description: "Default false."
10760
10803
  },
10761
10804
  include_deps: {
10762
10805
  type: "boolean",
10763
- 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."
10764
10807
  },
10765
10808
  context: {
10766
10809
  type: "integer",
10767
- 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."
10768
10811
  },
10769
10812
  summary_only: {
10770
10813
  type: "boolean",
10771
- 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."
10772
10815
  }
10773
10816
  },
10774
10817
  required: ["pattern"]
@@ -10881,7 +10924,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10881
10924
  });
10882
10925
  registry.register({
10883
10926
  name: "multi_edit",
10884
- 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.",
10885
10928
  parameters: {
10886
10929
  type: "object",
10887
10930
  properties: {
@@ -11023,19 +11066,33 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
11023
11066
  }
11024
11067
 
11025
11068
  // src/tools/plan-core.ts
11026
- 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.";
11027
- 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.";
11028
- 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.";
11029
11072
  var STEP_ITEM_SCHEMA = {
11030
11073
  type: "object",
11031
11074
  properties: {
11032
11075
  id: { type: "string", description: "Stable id, e.g. step-1." },
11033
11076
  title: { type: "string", description: "Short imperative title." },
11034
- action: { type: "string", description: "One-sentence description of the concrete action." },
11077
+ action: { type: "string", description: "One-sentence concrete action." },
11035
11078
  risk: {
11036
11079
  type: "string",
11037
11080
  enum: ["low", "med", "high"],
11038
- 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" }
11039
11096
  }
11040
11097
  },
11041
11098
  required: ["id", "title", "action"]
@@ -11057,10 +11114,42 @@ function sanitizeSteps(raw) {
11057
11114
  const step = { id, title, action };
11058
11115
  const risk = sanitizeRisk(e.risk);
11059
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;
11060
11123
  steps.push(step);
11061
11124
  }
11062
11125
  return steps.length > 0 ? steps : void 0;
11063
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
+ }
11064
11153
  function registerSubmitPlan(registry, opts) {
11065
11154
  registry.register({
11066
11155
  name: "submit_plan",
@@ -11071,16 +11160,16 @@ function registerSubmitPlan(registry, opts) {
11071
11160
  properties: {
11072
11161
  plan: {
11073
11162
  type: "string",
11074
- 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."
11075
11164
  },
11076
11165
  steps: {
11077
11166
  type: "array",
11078
- 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, ...).",
11079
11168
  items: STEP_ITEM_SCHEMA
11080
11169
  },
11081
11170
  summary: {
11082
11171
  type: "string",
11083
- 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."
11084
11173
  }
11085
11174
  },
11086
11175
  required: ["plan"]
@@ -11118,19 +11207,33 @@ function registerMarkStepComplete(registry, opts) {
11118
11207
  properties: {
11119
11208
  stepId: {
11120
11209
  type: "string",
11121
- 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."
11122
11211
  },
11123
11212
  title: {
11124
11213
  type: "string",
11125
- 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."
11126
11215
  },
11127
11216
  result: {
11128
11217
  type: "string",
11129
- description: "One-sentence summary of what was done for this step."
11218
+ description: "One-sentence summary of what was done."
11130
11219
  },
11131
11220
  notes: {
11132
11221
  type: "string",
11133
- 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
+ }
11134
11237
  }
11135
11238
  },
11136
11239
  required: ["stepId", "result"]
@@ -11148,9 +11251,15 @@ function registerMarkStepComplete(registry, opts) {
11148
11251
  }
11149
11252
  const title = typeof args?.title === "string" ? args.title.trim() || void 0 : void 0;
11150
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
+ }
11151
11259
  const update = { kind: "step_completed", stepId, result };
11152
11260
  if (title) update.title = title;
11153
11261
  if (notes) update.notes = notes;
11262
+ if (evidence) update.evidence = evidence;
11154
11263
  opts.onStepCompleted?.(update);
11155
11264
  const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
11156
11265
  kind: "plan_checkpoint",
@@ -11175,16 +11284,16 @@ function registerRevisePlan(registry, opts) {
11175
11284
  properties: {
11176
11285
  reason: {
11177
11286
  type: "string",
11178
- 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."
11179
11288
  },
11180
11289
  remainingSteps: {
11181
11290
  type: "array",
11182
- 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.",
11183
11292
  items: STEP_ITEM_SCHEMA
11184
11293
  },
11185
11294
  summary: {
11186
11295
  type: "string",
11187
- description: "Optional. Updated one-line plan summary if the overall framing has shifted."
11296
+ description: "Optional. Updated one-line summary when framing has shifted."
11188
11297
  }
11189
11298
  },
11190
11299
  required: ["reason", "remainingSteps"]
@@ -11222,7 +11331,7 @@ function registerPlanTool(registry, opts = {}) {
11222
11331
  }
11223
11332
 
11224
11333
  // src/tools/todo.ts
11225
- 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.";
11226
11335
  function validateTodos(raw) {
11227
11336
  if (!Array.isArray(raw)) {
11228
11337
  throw new Error("todo_write: `todos` must be an array");
@@ -11848,4 +11957,4 @@ export {
11848
11957
  he/he.js:
11849
11958
  (*! https://mths.be/he v1.2.0 by @mathias | MIT license *)
11850
11959
  */
11851
- //# sourceMappingURL=chunk-ICAFSZHS.js.map
11960
+ //# sourceMappingURL=chunk-JBH5RM7X.js.map