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
package/dist/index.js CHANGED
@@ -472,6 +472,12 @@ function loadMetasoApiKey(path2 = defaultConfigPath()) {
472
472
  if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
473
473
  return DEFAULT_METASO_API_KEY;
474
474
  }
475
+ function loadTavilyApiKey(path2 = defaultConfigPath()) {
476
+ if (process.env.TAVILY_API_KEY) return process.env.TAVILY_API_KEY.trim();
477
+ const cfg = readConfig(path2).tavilyApiKey;
478
+ if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
479
+ return void 0;
480
+ }
475
481
  function defaultConfigPath() {
476
482
  return join(homedir(), ".reasonix", "config.json");
477
483
  }
@@ -573,6 +579,7 @@ function webSearchEngine(path2 = defaultConfigPath()) {
573
579
  const cfg = readConfig(path2).webSearchEngine;
574
580
  if (cfg === "searxng") return "searxng";
575
581
  if (cfg === "metaso") return "metaso";
582
+ if (cfg === "tavily") return "tavily";
576
583
  return "mojeek";
577
584
  }
578
585
  function webSearchEndpoint(path2 = defaultConfigPath()) {
@@ -799,7 +806,7 @@ var DeepSeekClient = class {
799
806
  if (opts.temperature !== void 0) payload.temperature = opts.temperature;
800
807
  if (opts.maxTokens !== void 0) payload.max_tokens = opts.maxTokens;
801
808
  if (opts.responseFormat) payload.response_format = opts.responseFormat;
802
- if (opts.thinking) {
809
+ if (opts.thinking && !this._isAzureEndpoint()) {
803
810
  payload.extra_body = { thinking: { type: opts.thinking } };
804
811
  }
805
812
  if (opts.reasoningEffort) {
@@ -807,6 +814,17 @@ var DeepSeekClient = class {
807
814
  }
808
815
  return payload;
809
816
  }
817
+ /** Azure OpenAI-compatible endpoints do not accept DeepSeek's proprietary
818
+ * `extra_body.thinking` field (they reject the request with 400). We still
819
+ * send `reasoning_effort`, which Azure *does* support. */
820
+ _isAzureEndpoint() {
821
+ try {
822
+ const host = new URL(this.baseUrl).hostname;
823
+ return host === "azure.com" || host.endsWith(".azure.com");
824
+ } catch {
825
+ return false;
826
+ }
827
+ }
810
828
  /** Returns null on failure so callers can degrade — session must keep working without balance UI. */
811
829
  async getBalance(opts = {}) {
812
830
  try {
@@ -1191,8 +1209,9 @@ var EN = {
1191
1209
  { key: "wheel", text: "scrolls chat history (works on web/cloud/SSH terminals too)" },
1192
1210
  {
1193
1211
  key: "\u2191 / \u2193",
1194
- text: "scroll chat \xB7 use Ctrl+P / Ctrl+N for prompt history + multi-line cursor"
1195
- }
1212
+ text: "prompt history (or per-line cursor in a multi-line draft) \u2014 Ctrl+P / Ctrl+N alias"
1213
+ },
1214
+ { key: "PgUp / PgDn", text: "scroll chat history (mouse wheel routes here too)" }
1196
1215
  ]
1197
1216
  }
1198
1217
  ],
@@ -1206,11 +1225,11 @@ var EN = {
1206
1225
  rows: [
1207
1226
  { key: "Enter", text: "submit the prompt" },
1208
1227
  { key: "Shift+Enter", text: "insert a newline in the prompt" },
1209
- { key: "\u2191 / \u2193", text: "scroll chat history (mouse wheel routes here too)" },
1210
1228
  {
1211
- key: "Ctrl+P / Ctrl+N",
1229
+ key: "\u2191 / \u2193",
1212
1230
  text: "previous / next prompt history \xB7 cursor up / down in a multi-line draft"
1213
1231
  },
1232
+ { key: "Ctrl+P / Ctrl+N", text: "readline alias for \u2191 / \u2193" },
1214
1233
  { key: "Ctrl+A / Ctrl+E", text: "jump to start / end of the current line" },
1215
1234
  { key: "Ctrl+W", text: "delete the word before the cursor" },
1216
1235
  { key: "Ctrl+U", text: "clear the entire prompt buffer" },
@@ -1219,7 +1238,11 @@ var EN = {
1219
1238
  { key: "Esc", text: "dismiss picker \xB7 abort the running model turn" },
1220
1239
  { key: "Ctrl+C", text: "abort the running model turn (NOT copy \u2014 see clipboard)" },
1221
1240
  { key: "PgUp / PgDn", text: "scroll chat history a page at a time" },
1222
- { key: "End", text: "jump chat to the most recent line" }
1241
+ { key: "End", text: "jump chat to the most recent line" },
1242
+ {
1243
+ key: "Ctrl+R",
1244
+ text: "toggle verbose mode \u2014 full reasoning + tool output, no head/tail elision"
1245
+ }
1223
1246
  ]
1224
1247
  },
1225
1248
  {
@@ -1258,7 +1281,7 @@ var EN = {
1258
1281
  ]
1259
1282
  }
1260
1283
  ],
1261
- footer: "Wheel\u2192\u2191/\u2193 via DECSET 1007 (alternate-scroll) \u2014 wheel scrolls chat on most terminals (web/cloud/SSH included) without disturbing native selection. Drag to select stays modifier-free. Pass --no-mouse to opt out."
1284
+ footer: "Wheel scrolls chat on most terminals (web/cloud/SSH included) \u2014 SGR mouse tracking is on by default and stays out of the way of native drag-select and right-click. Pass --no-mouse to opt out."
1262
1285
  },
1263
1286
  tipShownOnce: "shown once",
1264
1287
  modelOverride: "override the default model",
@@ -1413,7 +1436,7 @@ var EN = {
1413
1436
  sessions: { description: "list saved sessions (current marked with \u25B8)" },
1414
1437
  title: { description: "ask the model to rename this session from the conversation" },
1415
1438
  qq: {
1416
- description: "connect, inspect, or disconnect the QQ channel for this session",
1439
+ description: "connect, inspect, or disconnect the QQ channel for this session (first connect guides App ID / App Secret setup)",
1417
1440
  argsHint: "[connect [appId appSecret [sandbox]]|status|disconnect]"
1418
1441
  },
1419
1442
  setup: { description: "reminds you to exit and run `reasonix setup`" },
@@ -1532,6 +1555,7 @@ var EN = {
1532
1555
  reviewSaveError: "Could not save config: {message}",
1533
1556
  reviewFooter: "[Enter] save \xB7 [Esc] cancel",
1534
1557
  savedTitle: "\u25B8 Saved.",
1558
+ savedShellHint: "Shell commands the model wants to run ask each time \u2014 pick `allow always` on the prompt to whitelist that exact command for this project. No global allow-all flag by design.",
1535
1559
  savedFooter: "[Enter] to exit",
1536
1560
  selectFooter: "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel",
1537
1561
  stepCounter: "Step {step}/{total} \xB7 ",
@@ -1596,6 +1620,8 @@ var EN = {
1596
1620
  title: "Checkpoint \u2014 step done",
1597
1621
  continue: "Continue \u2014 run the next step",
1598
1622
  continueHint: "Model resumes with the next step.",
1623
+ finish: "Finish \u2014 summarize and close",
1624
+ finishHint: "Model records the final step and summarizes the completed plan.",
1599
1625
  revise: "Revise \u2014 give feedback before the next step",
1600
1626
  reviseHint: "Stay paused, type guidance; model adjusts the remaining plan.",
1601
1627
  stop: "Stop \u2014 end the plan here",
@@ -1637,6 +1663,8 @@ var EN = {
1637
1663
  notedVerbCreated: "created",
1638
1664
  notedVerbAppended: "appended to",
1639
1665
  memoryWriteFailed: "# memory write failed",
1666
+ verboseOn: "\u25B8 verbose mode on \u2014 full reasoning + tool output",
1667
+ verboseOff: "\u25B8 verbose mode off \u2014 head/tail elision restored",
1640
1668
  commandFailed: "! command failed",
1641
1669
  btwUsage: "\u25B8 /btw <question> \u2014 ask a side question without polluting the conversation context.",
1642
1670
  btwHeader: "\u226B btw",
@@ -1734,6 +1762,10 @@ var EN = {
1734
1762
  helpShellDetail: " the conversation so the model sees it next turn.",
1735
1763
  helpShellConsent: " No allowlist gate \u2014 user-typed = explicit consent.",
1736
1764
  helpShellExample: " Example: !git status !ls src/ !npm test",
1765
+ helpShellGateTitle: "Model-invoked shell commands (per-call approval):",
1766
+ helpShellGate: " \u2191\u2193 + \u23CE each call shows a prompt with `allow once` / `allow always`",
1767
+ helpShellGateDetail: " / `deny`. Pick `allow always` to whitelist that exact",
1768
+ helpShellGatePolicy: " command prefix for this project. No global allow-all flag.",
1737
1769
  helpMemoryTitle: "Quick memory:",
1738
1770
  helpMemoryPin: " #<note> append <note> to <project>/REASONIX.md (committable).",
1739
1771
  helpMemoryPinEx: " Example: #findByEmail must be case-insensitive",
@@ -1771,6 +1803,48 @@ var EN = {
1771
1803
  titleStarted: "\u25B8 naming session\u2026",
1772
1804
  titleFailed: "\u25B8 session title failed: {reason}"
1773
1805
  },
1806
+ qq: {
1807
+ unavailable: "/qq is not available in this session.",
1808
+ connecting: "QQ: connecting\u2026",
1809
+ connectFailed: "QQ connect failed: {reason}",
1810
+ disconnecting: "QQ: disconnecting\u2026",
1811
+ disconnectFailed: "QQ disconnect failed: {reason}",
1812
+ usage: "Usage: /qq connect [appId appSecret [sandbox]] | /qq status | /qq disconnect",
1813
+ promptAppId: "QQ setup: enter your QQ Open Platform App ID, then press Enter. Type /cancel to abort.",
1814
+ promptAppSecret: "QQ setup: enter your QQ Open Platform App Secret, then press Enter. Type /cancel to abort.",
1815
+ setupWaitingAppId: "waiting for App ID",
1816
+ setupWaitingAppSecret: "waiting for App Secret",
1817
+ setupCancelled: "QQ setup cancelled.",
1818
+ credentialsRequired: "QQ App ID and App Secret are required.",
1819
+ connected: "QQ connected in {mode} mode. It will auto-start on future launches.",
1820
+ alreadyConnected: "QQ is already connected in {mode} mode. Auto-start is enabled.",
1821
+ disconnected: "QQ disconnected. Auto-start is disabled.",
1822
+ status: "QQ: {connected}, auto-start {enabled}, credentials {configured}, appId {appId}, {sandbox}, access {access}, current mode {mode}.",
1823
+ statusSetup: "QQ: setup in progress \u2014 {step}",
1824
+ stateConnected: "connected",
1825
+ stateDisconnected: "disconnected",
1826
+ stateEnabled: "enabled",
1827
+ stateDisabled: "disabled",
1828
+ stateConfigured: "configured",
1829
+ stateNotConfigured: "not configured",
1830
+ sandbox: "sandbox",
1831
+ production: "production",
1832
+ none: "none",
1833
+ modeChat: "chat",
1834
+ modeCode: "code",
1835
+ accessOwner: "owner {owner}",
1836
+ accessOwnerWithAllowlist: "owner {owner}, allowlist {count}",
1837
+ accessAllowlist: "allowlist {count}",
1838
+ accessRuntime: "first-sender (runtime only, {owner})",
1839
+ accessOpen: "open (unbound)",
1840
+ lockAlreadyRunning: "QQ channel is already running in process {pid}. Stop that process before starting another QQ channel.",
1841
+ unauthorizedMessage: "QQ ignored message from unauthorized openid {openid}. Current access: {access}.",
1842
+ runtimeBound: "QQ temporarily bound this run to first sender {openid}. Set `qq.ownerOpenId` in config to persist access.",
1843
+ missingAppId: "QQ App ID is required. Run `/qq connect` to configure.",
1844
+ missingAppSecret: "QQ App Secret is required. Run `/qq connect` to configure.",
1845
+ authFailed: "QQ bot authentication failed \u2014 check your App ID and App Secret.",
1846
+ readyTimeout: "QQ bot did not receive READY within 15s \u2014 check your App ID and App Secret."
1847
+ },
1774
1848
  admin: {
1775
1849
  doctorNeedsTui: "/doctor needs a TUI context (postDoctor wired).",
1776
1850
  doctorRunning: "\u2695 Doctor \u2014 running health checks\u2026",
@@ -2003,7 +2077,7 @@ var EN = {
2003
2077
  changesNoteShort: "Changes take effect on next /new or launch. Subcommands: /memory list | show | forget | clear"
2004
2078
  },
2005
2079
  mcp: {
2006
- noServers: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.',
2080
+ noServers: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog. Note: model-invoked shell commands are gated per-call (allow once / allow always / deny) \u2014 no global allow-all flag.',
2007
2081
  toolsLabel: " tools {count}",
2008
2082
  resourcesHint: "`/resource` to browse+read",
2009
2083
  promptsHint: "`/prompt` to browse+fetch",
@@ -2037,12 +2111,14 @@ var EN = {
2037
2111
  usageSearxng: " /search-engine searxng use SearXNG at default endpoint",
2038
2112
  usageSearxngUrl: " /search-engine searxng <url> use SearXNG at custom endpoint",
2039
2113
  usageMetaso: " /search-engine metaso use Metaso API (100/d free, configure your own API key for more)",
2114
+ usageTavily: " /search-engine tavily use Tavily API (LLM-friendly, free 1000/mo \u2014 set TAVILY_API_KEY or tavilyApiKey in config; get one at https://tavily.com)",
2040
2115
  alias: "Alias: /se",
2041
2116
  searxngInfo: "SearXNG is a self-hosted metasearch engine (https://github.com/searxng/searxng).",
2042
2117
  searxngInstall: "Install it with: docker run -d -p 8080:8080 searxng/searxng",
2043
2118
  switched: 'Switched web search engine to "{engine}".{note}',
2044
2119
  switchedSearxngNote: " Make sure SearXNG is running at {endpoint}.",
2045
2120
  switchedMetasoNote: " There is a daily quota of 100 (configure your own API key for higher limits).",
2121
+ switchedTavilyNote: " Set TAVILY_API_KEY or `tavilyApiKey` in config; free 1000/mo at https://tavily.com.",
2046
2122
  confirmed: '\u2713 Web search engine set to "{engine}"{detail}. Next assistant turn will pick up the change.',
2047
2123
  confirmedDetail: " ({endpoint})"
2048
2124
  },
@@ -2178,6 +2254,13 @@ var EN = {
2178
2254
  linesBelow: " \u2193 {count} line below (\u2193/j or Space/PgDn)",
2179
2255
  linesBelowPlural: " \u2193 {count} lines below (\u2193/j or Space/PgDn)"
2180
2256
  },
2257
+ editPicker: {
2258
+ title: "edit a previous message",
2259
+ hint: "\u2191\u2193 pick \xB7 Enter to load into composer \xB7 Esc to cancel",
2260
+ empty: "no user turns yet \u2014 nothing to edit",
2261
+ dismiss: "Esc to dismiss",
2262
+ forked: "\u25B8 forked at turn #{turn} \u2014 buffer holds the original text"
2263
+ },
2181
2264
  sessionPicker: {
2182
2265
  header: " \u25C8 REASONIX \xB7 pick a session ",
2183
2266
  title: "pick a session \u2014 {workspace}",
@@ -2311,6 +2394,11 @@ var EN = {
2311
2394
  metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek",
2312
2395
  metasoParseError: "web_search: Metaso returned unparseable response (HTTP {status}) \u2014 try again later",
2313
2396
  metasoApiError: "web_search: Metaso API error (code {code}: {message}) \u2014 try again later",
2397
+ tavilyMissingKey: "web_search: Tavily backend requires an API key \u2014 set TAVILY_API_KEY env var or `tavilyApiKey` in ~/.reasonix/config.json; free 1000/mo signup at https://tavily.com",
2398
+ tavilyUnauthorized: "web_search: Tavily API key rejected \u2014 check TAVILY_API_KEY or get one at https://tavily.com",
2399
+ tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine mojeek, or upgrade your Tavily plan",
2400
+ tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek",
2401
+ tavilyParseError: "web_search: Tavily returned unparseable response (HTTP {status}) \u2014 try again later",
2314
2402
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2315
2403
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
2316
2404
  fetchForbidden403: "web_fetch 403 for {url} \u2014 try: the host is blocking this client; the page may require login or block bots \u2014 use web_search snippets instead",
@@ -2420,7 +2508,7 @@ var EN = {
2420
2508
  slow: "slow \xB7 {ms}ms",
2421
2509
  verySlow: "very slow \xB7 {ms}ms",
2422
2510
  slowToast: "\u26A0 MCP `{name}` slow \xB7 {seconds}s p95 over the last {sampleSize} calls",
2423
- emptyHint: "\u2139 no MCP servers configured \u2014 try: `reasonix setup` to re-pick, or `reasonix mcp install filesystem`"
2511
+ emptyHint: "\u2139 no MCP servers configured \u2014 try: `reasonix setup` to re-pick, or `reasonix mcp install filesystem` \xB7 shell commands gate per-call (allow once / allow always / deny), no global allow-all"
2424
2512
  },
2425
2513
  denyContextInput: {
2426
2514
  description: "Tell the agent why you denied this. The next attempt will see your reason as additional context."
@@ -2429,7 +2517,8 @@ var EN = {
2429
2517
  scrollAbove: " \u2191 {scroll} / {max} row above",
2430
2518
  scrollAbovePlural: " \u2191 {scroll} / {max} rows above",
2431
2519
  scrollMore: " \u2014 {remaining} more",
2432
- scrollPgUp: " \xB7 PgUp / wheel / \u2191"
2520
+ scrollPgUp: " \xB7 PgUp / wheel",
2521
+ scrollCopy: " \xB7 /copy enters copy mode"
2433
2522
  },
2434
2523
  slashArgPicker: {
2435
2524
  noMatch: 'no match for "{partial}"',
@@ -2487,7 +2576,8 @@ var EN = {
2487
2576
  reconnectDetail: "tearing down \xB7 re-handshake \xB7 listing tools",
2488
2577
  disabledDetail: "via /mcp disable {name}",
2489
2578
  failedSetupHint: "\u2192 run `reasonix setup` to remove this entry, or fix the underlying issue (missing npm package, network, etc.).",
2490
- failedSetupConfigHint: "\u2192 run `reasonix setup` to remove broken entries from your saved config."
2579
+ failedSetupConfigHint: "\u2192 run `reasonix setup` to remove broken entries from your saved config.",
2580
+ abortedHint: "MCP startup aborted \u2014 {count} server(s) skipped. Run /mcp to retry once you've fixed the underlying issue."
2491
2581
  },
2492
2582
  checkpointPicker: {
2493
2583
  title: "restore a checkpoint \u2014 {workspace}",
@@ -2632,8 +2722,9 @@ var zhCN = {
2632
2722
  { key: "\u6EDA\u8F6E", text: "\u6EDA\u52A8\u804A\u5929\u8BB0\u5F55\uFF08Web / \u4E91\u7AEF / SSH \u7EC8\u7AEF\u4E5F\u80FD\u7528\uFF09" },
2633
2723
  {
2634
2724
  key: "\u2191 / \u2193",
2635
- text: "\u6EDA\u52A8\u804A\u5929 \xB7 \u8F93\u5165\u6846\u5386\u53F2 + \u591A\u884C\u5149\u6807\u7528 Ctrl+P / Ctrl+N"
2636
- }
2725
+ text: "\u8F93\u5165\u5386\u53F2\uFF08\u591A\u884C\u8349\u7A3F\u65F6\u6309\u884C\u79FB\u52A8\u5149\u6807\uFF09\u2014 Ctrl+P / Ctrl+N \u540C\u4E49"
2726
+ },
2727
+ { key: "PgUp / PgDn", text: "\u6EDA\u52A8\u804A\u5929\u8BB0\u5F55\uFF08\u9F20\u6807\u6EDA\u8F6E\u4E5F\u8D70\u8FD9\u6761\u8DEF\u5F84\uFF09" }
2637
2728
  ]
2638
2729
  }
2639
2730
  ],
@@ -2647,11 +2738,11 @@ var zhCN = {
2647
2738
  rows: [
2648
2739
  { key: "Enter", text: "\u63D0\u4EA4\u8F93\u5165" },
2649
2740
  { key: "Shift+Enter", text: "\u5728\u8F93\u5165\u6846\u4E2D\u63D2\u5165\u6362\u884C" },
2650
- { key: "\u2191 / \u2193", text: "\u6EDA\u52A8\u804A\u5929\u8BB0\u5F55\uFF08\u9F20\u6807\u6EDA\u8F6E\u4E5F\u8D70\u8FD9\u6761\u8DEF\u5F84\uFF09" },
2651
2741
  {
2652
- key: "Ctrl+P / Ctrl+N",
2742
+ key: "\u2191 / \u2193",
2653
2743
  text: "\u4E0A\u4E00\u6761 / \u4E0B\u4E00\u6761\u8F93\u5165\u5386\u53F2 \xB7 \u591A\u884C\u8349\u7A3F\u4E2D\u6309\u884C\u79FB\u52A8\u5149\u6807"
2654
2744
  },
2745
+ { key: "Ctrl+P / Ctrl+N", text: "\u2191 / \u2193 \u7684 readline \u540C\u4E49\u952E" },
2655
2746
  { key: "Ctrl+A / Ctrl+E", text: "\u8DF3\u5230\u5F53\u524D\u884C\u7684\u5F00\u5934 / \u7ED3\u5C3E" },
2656
2747
  { key: "Ctrl+W", text: "\u5220\u9664\u5149\u6807\u524D\u7684\u4E00\u4E2A\u8BCD" },
2657
2748
  { key: "Ctrl+U", text: "\u6E05\u7A7A\u6574\u4E2A\u8F93\u5165\u7F13\u51B2\u533A" },
@@ -2660,7 +2751,8 @@ var zhCN = {
2660
2751
  { key: "Esc", text: "\u5173\u95ED\u5F39\u51FA\u9009\u62E9\u5668 \xB7 \u4E2D\u6B62\u5F53\u524D\u6A21\u578B\u56DE\u5408" },
2661
2752
  { key: "Ctrl+C", text: "\u4E2D\u6B62\u5F53\u524D\u6A21\u578B\u56DE\u5408\uFF08\u4E0D\u662F\u590D\u5236 \u2014 \u89C1\u526A\u8D34\u677F\u6BB5\uFF09" },
2662
2753
  { key: "PgUp / PgDn", text: "\u6574\u9875\u6EDA\u52A8\u804A\u5929\u8BB0\u5F55" },
2663
- { key: "End", text: "\u8DF3\u5230\u804A\u5929\u7684\u6700\u65B0\u4E00\u884C" }
2754
+ { key: "End", text: "\u8DF3\u5230\u804A\u5929\u7684\u6700\u65B0\u4E00\u884C" },
2755
+ { key: "Ctrl+R", text: "\u5207\u6362\u8BE6\u7EC6\u6A21\u5F0F \u2014 \u663E\u793A\u5B8C\u6574\u63A8\u7406 + \u5DE5\u5177\u8F93\u51FA\uFF0C\u4E0D\u7701\u7565" }
2664
2756
  ]
2665
2757
  },
2666
2758
  {
@@ -2699,7 +2791,7 @@ var zhCN = {
2699
2791
  ]
2700
2792
  }
2701
2793
  ],
2702
- footer: "\u901A\u8FC7 DECSET 1007\uFF08alternate-scroll\uFF09\uFF0C\u7EC8\u7AEF\u628A\u6EDA\u8F6E\u7FFB\u8BD1\u6210 \u2191/\u2193 \u53D1\u7ED9\u5E94\u7528 \u2014 \u5927\u591A\u6570\u7EC8\u7AEF\uFF08\u542B Web / \u4E91\u7AEF / SSH\uFF09\u90FD\u80FD\u6EDA\uFF0C\u4E14\u4E0D\u5F71\u54CD\u7EC8\u7AEF\u539F\u751F\u9009\u533A\u3002\u76F4\u63A5\u62D6\u52A8\u9009\u4E2D\u6587\u672C\u65E0\u9700 Shift\u3002\u4F20\u5165 --no-mouse \u53EF\u5173\u95ED\u3002"
2794
+ footer: "\u6EDA\u8F6E\u5728\u5927\u591A\u6570\u7EC8\u7AEF\uFF08\u542B Web / \u4E91\u7AEF / SSH\uFF09\u90FD\u80FD\u6EDA\u804A\u5929 \u2014 \u9ED8\u8BA4\u5F00\u542F SGR \u9F20\u6807\u8DDF\u8E2A\uFF0C\u4F46\u4E0D\u4F1A\u5F71\u54CD\u7EC8\u7AEF\u539F\u751F\u62D6\u9009\u548C\u53F3\u952E\u83DC\u5355\u3002\u76F4\u63A5\u62D6\u52A8\u9009\u4E2D\u6587\u672C\u65E0\u9700 Shift\u3002\u4F20\u5165 --no-mouse \u53EF\u5173\u95ED\u3002"
2703
2795
  },
2704
2796
  tipShownOnce: "\u4EC5\u663E\u793A\u4E00\u6B21",
2705
2797
  modelOverride: "\u8986\u76D6\u9ED8\u8BA4\u6A21\u578B",
@@ -2856,7 +2948,7 @@ var zhCN = {
2856
2948
  sessions: { description: "\u5217\u51FA\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD\uFF08\u5F53\u524D\u6807\u8BB0\u4E3A \u25B8\uFF09" },
2857
2949
  title: { description: "\u8BA9\u6A21\u578B\u6839\u636E\u5F53\u524D\u5BF9\u8BDD\u91CD\u547D\u540D\u6B64\u4F1A\u8BDD" },
2858
2950
  qq: {
2859
- description: "\u8FDE\u63A5\u3001\u67E5\u770B\u6216\u65AD\u5F00\u5F53\u524D\u4F1A\u8BDD\u7684 QQ \u901A\u9053",
2951
+ description: "\u8FDE\u63A5\u3001\u67E5\u770B\u6216\u65AD\u5F00\u5F53\u524D\u4F1A\u8BDD\u7684 QQ \u901A\u9053\uFF08\u9996\u6B21\u8FDE\u63A5\u4F1A\u5F15\u5BFC\u5F55\u5165 App ID / App Secret\uFF09",
2860
2952
  argsHint: "[connect [appId appSecret [sandbox]]|status|disconnect]"
2861
2953
  },
2862
2954
  setup: { description: "\u63D0\u9192\u60A8\u9000\u51FA\u5E76\u8FD0\u884C `reasonix setup`" },
@@ -2977,6 +3069,7 @@ var zhCN = {
2977
3069
  reviewSaveError: "\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25\uFF1A{message}",
2978
3070
  reviewFooter: "[Enter] \u4FDD\u5B58 \xB7 [Esc] \u53D6\u6D88",
2979
3071
  savedTitle: "\u25B8 \u5DF2\u4FDD\u5B58\u3002",
3072
+ savedShellHint: "\u6A21\u578B\u53D1\u8D77\u7684 shell \u547D\u4EE4\u6BCF\u6B21\u90FD\u4F1A\u5F39\u51FA\u786E\u8BA4 \u2014\u2014 \u5728\u63D0\u793A\u6846\u91CC\u9009 `allow always` \u53EF\u5C06\u8BE5\u547D\u4EE4\u524D\u7F00\u52A0\u5165\u672C\u9879\u76EE\u767D\u540D\u5355\u3002\u8BBE\u8BA1\u4E0A\u6CA1\u6709\u300C\u5168\u5C40\u653E\u884C\u300D\u5F00\u5173\u3002",
2980
3073
  savedFooter: "[Enter] \u9000\u51FA",
2981
3074
  selectFooter: "[\u2191\u2193] \u79FB\u52A8 \xB7 [Enter] \u786E\u8BA4 \xB7 [Esc] \u53D6\u6D88",
2982
3075
  stepCounter: "\u6B65\u9AA4 {step}/{total} \xB7 ",
@@ -3041,6 +3134,8 @@ var zhCN = {
3041
3134
  title: "\u68C0\u67E5\u70B9 \u2014\u2014 \u5F53\u524D\u6B65\u9AA4\u5DF2\u5B8C\u6210",
3042
3135
  continue: "\u7EE7\u7EED \u2014\u2014 \u6267\u884C\u4E0B\u4E00\u6B65",
3043
3136
  continueHint: "\u6A21\u578B\u4ECE\u4E0B\u4E00\u6B65\u7EE7\u7EED\u3002",
3137
+ finish: "\u5B8C\u6210 \u2014\u2014 \u603B\u7ED3\u5E76\u6536\u5C3E",
3138
+ finishHint: "\u6A21\u578B\u8BB0\u5F55\u6700\u540E\u4E00\u6B65\uFF0C\u7136\u540E\u603B\u7ED3\u5DF2\u5B8C\u6210\u7684\u8BA1\u5212\u3002",
3044
3139
  revise: "\u8C03\u6574 \u2014\u2014 \u5728\u4E0B\u4E00\u6B65\u524D\u7ED9\u53CD\u9988",
3045
3140
  reviseHint: "\u5148\u6682\u505C\uFF0C\u8F93\u5165\u6307\u5F15\uFF1B\u6A21\u578B\u4F1A\u8C03\u6574\u5269\u4F59\u8BA1\u5212\u3002",
3046
3141
  stop: "\u505C\u6B62 \u2014\u2014 \u5728\u6B64\u7ED3\u675F\u8BA1\u5212",
@@ -3082,6 +3177,8 @@ var zhCN = {
3082
3177
  notedVerbCreated: "\u521B\u5EFA",
3083
3178
  notedVerbAppended: "\u8FFD\u52A0\u5230",
3084
3179
  memoryWriteFailed: "# \u8BB0\u5FC6\u5199\u5165\u5931\u8D25",
3180
+ verboseOn: "\u25B8 \u8BE6\u7EC6\u6A21\u5F0F\u5DF2\u5F00 \u2014 \u663E\u793A\u5B8C\u6574\u63A8\u7406 + \u5DE5\u5177\u8F93\u51FA",
3181
+ verboseOff: "\u25B8 \u8BE6\u7EC6\u6A21\u5F0F\u5DF2\u5173 \u2014 \u6062\u590D\u5934\u5C3E\u7701\u7565",
3085
3182
  commandFailed: "! \u547D\u4EE4\u5931\u8D25",
3086
3183
  btwUsage: "\u25B8 /btw <\u95EE\u9898> \u2014 \u987A\u4FBF\u95EE\u4E2A\u9898\u5916\u8BDD\uFF0C\u4E0D\u4F1A\u5199\u5165\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002",
3087
3184
  btwHeader: "\u226B btw",
@@ -3179,6 +3276,10 @@ var zhCN = {
3179
3276
  helpShellDetail: " \u4EE5\u4FBF\u6A21\u578B\u5728\u4E0B\u4E00\u8F6E\u770B\u5230\u3002\u65E0\u5141\u8BB8\u5217\u8868\u9650\u5236\u3002",
3180
3277
  helpShellConsent: " \u7528\u6237\u8F93\u5165 = \u660E\u786E\u540C\u610F\u3002",
3181
3278
  helpShellExample: " \u793A\u4F8B\uFF1A!git status !ls src/ !npm test",
3279
+ helpShellGateTitle: "\u6A21\u578B\u53D1\u8D77\u7684 shell \u547D\u4EE4\uFF08\u6309\u6B21\u5BA1\u6279\uFF09\uFF1A",
3280
+ helpShellGate: " \u2191\u2193 + \u23CE \u6BCF\u6B21\u90FD\u4F1A\u5F39\u51FA `allow once` / `allow always` /",
3281
+ helpShellGateDetail: " `deny` \u4E09\u9009\u4E00\u3002\u9009 `allow always` \u53EF\u5C06\u8BE5\u547D\u4EE4\u524D\u7F00",
3282
+ helpShellGatePolicy: " \u52A0\u5165\u672C\u9879\u76EE\u767D\u540D\u5355\u3002\u8BBE\u8BA1\u4E0A\u6CA1\u6709\u300C\u5168\u5C40\u653E\u884C\u300D\u5F00\u5173\u3002",
3182
3283
  helpMemoryTitle: "\u5FEB\u901F\u8BB0\u5FC6\uFF1A",
3183
3284
  helpMemoryPin: " #<note> \u5C06 <note> \u8FFD\u52A0\u5230 <project>/REASONIX.md\uFF08\u53EF\u63D0\u4EA4\uFF09\u3002",
3184
3285
  helpMemoryPinEx: " \u793A\u4F8B\uFF1A#findByEmail \u5FC5\u987B\u533A\u5206\u5927\u5C0F\u5199",
@@ -3216,6 +3317,48 @@ var zhCN = {
3216
3317
  titleStarted: "\u25B8 \u6B63\u5728\u547D\u540D\u4F1A\u8BDD\u2026",
3217
3318
  titleFailed: "\u25B8 \u4F1A\u8BDD\u547D\u540D\u5931\u8D25\uFF1A{reason}"
3218
3319
  },
3320
+ qq: {
3321
+ unavailable: "/qq \u5728\u5F53\u524D\u4F1A\u8BDD\u4E2D\u4E0D\u53EF\u7528\u3002",
3322
+ connecting: "QQ\uFF1A\u6B63\u5728\u8FDE\u63A5\u2026",
3323
+ connectFailed: "QQ \u8FDE\u63A5\u5931\u8D25\uFF1A{reason}",
3324
+ disconnecting: "QQ\uFF1A\u6B63\u5728\u65AD\u5F00\u2026",
3325
+ disconnectFailed: "QQ \u65AD\u5F00\u5931\u8D25\uFF1A{reason}",
3326
+ usage: "\u7528\u6CD5\uFF1A/qq connect [appId appSecret [sandbox]] | /qq status | /qq disconnect",
3327
+ promptAppId: "QQ \u9996\u6B21\u914D\u7F6E\uFF1A\u8BF7\u8F93\u5165 QQ \u5F00\u653E\u5E73\u53F0 App ID \u540E\u56DE\u8F66\u3002\u8F93\u5165 /cancel \u53EF\u53D6\u6D88\u3002",
3328
+ promptAppSecret: "QQ \u9996\u6B21\u914D\u7F6E\uFF1A\u8BF7\u8F93\u5165 QQ \u5F00\u653E\u5E73\u53F0 App Secret \u540E\u56DE\u8F66\u3002\u8F93\u5165 /cancel \u53EF\u53D6\u6D88\u3002",
3329
+ setupWaitingAppId: "\u7B49\u5F85\u8F93\u5165 App ID",
3330
+ setupWaitingAppSecret: "\u7B49\u5F85\u8F93\u5165 App Secret",
3331
+ setupCancelled: "QQ \u9996\u6B21\u914D\u7F6E\u5DF2\u53D6\u6D88\u3002",
3332
+ credentialsRequired: "QQ App ID \u548C App Secret \u4E0D\u80FD\u4E3A\u7A7A\u3002",
3333
+ connected: "QQ \u5DF2\u5728{mode}\u6A21\u5F0F\u4E0B\u8FDE\u63A5\u6210\u529F\uFF0C\u540E\u7EED\u542F\u52A8\u4F1A\u81EA\u52A8\u542F\u7528\u3002",
3334
+ alreadyConnected: "QQ \u5DF2\u5728{mode}\u6A21\u5F0F\u4E0B\u8FDE\u63A5\uFF0C\u81EA\u52A8\u542F\u52A8\u5DF2\u542F\u7528\u3002",
3335
+ disconnected: "QQ \u5DF2\u65AD\u5F00\u8FDE\u63A5\uFF0C\u81EA\u52A8\u542F\u52A8\u5DF2\u5173\u95ED\u3002",
3336
+ status: "QQ\uFF1A{connected}\uFF0C\u81EA\u52A8\u542F\u52A8{enabled}\uFF0C\u51ED\u636E{configured}\uFF0CappId {appId}\uFF0C{sandbox}\uFF0C\u8BBF\u95EE\u63A7\u5236 {access}\uFF0C\u5F53\u524D\u6A21\u5F0F {mode}\u3002",
3337
+ statusSetup: "QQ\uFF1A\u9996\u6B21\u914D\u7F6E\u8FDB\u884C\u4E2D \u2014\u2014 {step}",
3338
+ stateConnected: "\u5DF2\u8FDE\u63A5",
3339
+ stateDisconnected: "\u672A\u8FDE\u63A5",
3340
+ stateEnabled: "\u5DF2\u542F\u7528",
3341
+ stateDisabled: "\u672A\u542F\u7528",
3342
+ stateConfigured: "\u5DF2\u914D\u7F6E",
3343
+ stateNotConfigured: "\u672A\u914D\u7F6E",
3344
+ sandbox: "\u6C99\u7BB1\u73AF\u5883",
3345
+ production: "\u6B63\u5F0F\u73AF\u5883",
3346
+ none: "\u65E0",
3347
+ modeChat: "\u804A\u5929",
3348
+ modeCode: "\u4EE3\u7801",
3349
+ accessOwner: "\u6240\u6709\u8005 {owner}",
3350
+ accessOwnerWithAllowlist: "\u6240\u6709\u8005 {owner}\uFF0C\u767D\u540D\u5355 {count}",
3351
+ accessAllowlist: "\u767D\u540D\u5355 {count}",
3352
+ accessRuntime: "\u9996\u4E2A\u79C1\u804A\u7528\u6237\uFF08\u4EC5\u672C\u6B21\u8FD0\u884C\uFF0C{owner}\uFF09",
3353
+ accessOpen: "\u5F00\u653E\uFF08\u672A\u7ED1\u5B9A\uFF09",
3354
+ lockAlreadyRunning: "QQ \u901A\u9053\u5DF2\u5728\u8FDB\u7A0B {pid} \u4E2D\u8FD0\u884C\u3002\u8BF7\u5148\u505C\u6B62\u8BE5\u8FDB\u7A0B\uFF0C\u518D\u542F\u52A8\u65B0\u7684 QQ \u901A\u9053\u3002",
3355
+ unauthorizedMessage: "QQ \u5FFD\u7565\u4E86\u672A\u6388\u6743 openid {openid} \u7684\u6D88\u606F\u3002\u5F53\u524D\u8BBF\u95EE\u63A7\u5236\uFF1A{access}\u3002",
3356
+ runtimeBound: "QQ \u5DF2\u5728\u672C\u6B21\u8FD0\u884C\u4E2D\u4E34\u65F6\u7ED1\u5B9A\u5230\u9996\u4E2A\u53D1\u9001\u8005 {openid}\u3002\u5982\u9700\u6301\u4E45\u5316\uFF0C\u8BF7\u5728\u914D\u7F6E\u4E2D\u8BBE\u7F6E `qq.ownerOpenId`\u3002",
3357
+ missingAppId: "\u7F3A\u5C11 QQ App ID\u3002\u8BF7\u5148\u8FD0\u884C `/qq connect` \u5B8C\u6210\u914D\u7F6E\u3002",
3358
+ missingAppSecret: "\u7F3A\u5C11 QQ App Secret\u3002\u8BF7\u5148\u8FD0\u884C `/qq connect` \u5B8C\u6210\u914D\u7F6E\u3002",
3359
+ authFailed: "QQ \u673A\u5668\u4EBA\u9274\u6743\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 App ID \u548C App Secret\u3002",
3360
+ readyTimeout: "QQ \u673A\u5668\u4EBA 15 \u79D2\u5185\u672A\u6536\u5230 READY\uFF0C\u8BF7\u68C0\u67E5 App ID \u548C App Secret\u3002"
3361
+ },
3219
3362
  admin: {
3220
3363
  doctorNeedsTui: "/doctor \u9700\u8981 TUI \u4E0A\u4E0B\u6587\uFF08postDoctor \u5DF2\u8FDE\u63A5\uFF09\u3002",
3221
3364
  doctorRunning: "\u2695 \u5065\u5EB7\u68C0\u67E5 \u2014 \u6B63\u5728\u8FD0\u884C\u2026",
@@ -3448,7 +3591,7 @@ var zhCN = {
3448
3591
  changesNoteShort: "\u66F4\u6539\u5728\u4E0B\u6B21 /new \u6216\u542F\u52A8\u65F6\u751F\u6548\u3002\u5B50\u547D\u4EE4\uFF1A/memory list | show | forget | clear"
3449
3592
  },
3450
3593
  mcp: {
3451
- noServers: '\u672A\u9644\u52A0 MCP \u670D\u52A1\u5668\u3002\u8FD0\u884C `reasonix setup` \u9009\u62E9\u4E00\u4E9B\uFF0C\u6216\u4F7F\u7528 --mcp "<spec>" \u542F\u52A8\u3002`reasonix mcp list` \u663E\u793A\u76EE\u5F55\u3002',
3594
+ noServers: '\u672A\u9644\u52A0 MCP \u670D\u52A1\u5668\u3002\u8FD0\u884C `reasonix setup` \u9009\u62E9\u4E00\u4E9B\uFF0C\u6216\u4F7F\u7528 --mcp "<spec>" \u542F\u52A8\u3002`reasonix mcp list` \u663E\u793A\u76EE\u5F55\u3002\u6CE8\uFF1A\u6A21\u578B\u53D1\u8D77\u7684 shell \u547D\u4EE4\u6309\u6B21\u5BA1\u6279\uFF08allow once / allow always / deny\uFF09\uFF0C\u8BBE\u8BA1\u4E0A\u6CA1\u6709\u300C\u5168\u5C40\u653E\u884C\u300D\u5F00\u5173\u3002',
3452
3595
  toolsLabel: " \u5DE5\u5177 {count}",
3453
3596
  resourcesHint: "`/resource` \u6D4F\u89C8+\u8BFB\u53D6",
3454
3597
  promptsHint: "`/prompt` \u6D4F\u89C8+\u83B7\u53D6",
@@ -3482,12 +3625,14 @@ var zhCN = {
3482
3625
  usageSearxng: " /search-engine searxng \u4F7F\u7528 SearXNG \u9ED8\u8BA4\u7AEF\u70B9",
3483
3626
  usageSearxngUrl: " /search-engine searxng <url> \u4F7F\u7528 SearXNG \u81EA\u5B9A\u4E49\u7AEF\u70B9",
3484
3627
  usageMetaso: " /search-engine metaso \u4F7F\u7528 Metaso API\uFF08\u6BCF\u5929 100 \u6B21\u514D\u8D39\uFF0C\u914D\u7F6E\u4F60\u81EA\u5DF1\u7684 API \u5BC6\u94A5\u53EF\u63D0\u5347\u9650\u989D\uFF09",
3628
+ usageTavily: " /search-engine tavily \u4F7F\u7528 Tavily API\uFF08LLM \u53CB\u597D\uFF0C\u6BCF\u6708 1000 \u6B21\u514D\u8D39 \u2014 \u8BBE\u7F6E TAVILY_API_KEY \u6216 config \u7684 tavilyApiKey\uFF1B\u6CE8\u518C https://tavily.com\uFF09",
3485
3629
  alias: "\u522B\u540D\uFF1A/se",
3486
3630
  searxngInfo: "SearXNG \u662F\u4E00\u4E2A\u81EA\u6258\u7BA1\u7684\u5143\u641C\u7D22\u5F15\u64CE\uFF08https://github.com/searxng/searxng\uFF09\u3002",
3487
3631
  searxngInstall: "\u5B89\u88C5\u547D\u4EE4\uFF1A docker run -d -p 8080:8080 searxng/searxng",
3488
3632
  switched: '\u5DF2\u5207\u6362\u7F51\u9875\u641C\u7D22\u5F15\u64CE\u4E3A "{engine}"\u3002{note}',
3489
3633
  switchedSearxngNote: " \u8BF7\u786E\u4FDD SearXNG \u5728 {endpoint} \u8FD0\u884C\u3002",
3490
3634
  switchedMetasoNote: " \u6BCF\u65E5\u9650\u989D 100 \u6B21\uFF08\u914D\u7F6E\u4F60\u81EA\u5DF1\u7684 API \u5BC6\u94A5\u53EF\u63D0\u5347\u9650\u989D\uFF09\u3002",
3635
+ switchedTavilyNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF TAVILY_API_KEY \u6216 config \u4E2D\u7684 `tavilyApiKey`\uFF1Bhttps://tavily.com \u6BCF\u6708 1000 \u6B21\u514D\u8D39\u3002",
3491
3636
  confirmed: '\u2713 \u7F51\u9875\u641C\u7D22\u5F15\u64CE\u5DF2\u8BBE\u4E3A "{engine}"{detail}\u3002\u4E0B\u4E00\u8F6E\u6A21\u578B\u8C03\u7528\u5C06\u751F\u6548\u3002',
3492
3637
  confirmedDetail: "\uFF08{endpoint}\uFF09"
3493
3638
  },
@@ -3623,6 +3768,13 @@ var zhCN = {
3623
3768
  linesBelow: " \u2193 \u4E0B\u65B9 {count} \u884C\uFF08\u2193/j \u6216 Space/PgDn\uFF09",
3624
3769
  linesBelowPlural: " \u2193 \u4E0B\u65B9 {count} \u884C\uFF08\u2193/j \u6216 Space/PgDn\uFF09"
3625
3770
  },
3771
+ editPicker: {
3772
+ title: "\u7F16\u8F91\u4E4B\u524D\u7684\u6D88\u606F",
3773
+ hint: "\u2191\u2193 \u9009\u62E9 \xB7 Enter \u52A0\u8F7D\u5230\u8F93\u5165\u6846 \xB7 Esc \u53D6\u6D88",
3774
+ empty: "\u8FD8\u6CA1\u6709\u7528\u6237\u53D1\u8A00 \u2014 \u6CA1\u4EC0\u4E48\u53EF\u4EE5\u7F16\u8F91\u7684",
3775
+ dismiss: "Esc \u5173\u95ED",
3776
+ forked: "\u25B8 \u4ECE\u7B2C #{turn} \u8F6E\u5206\u53C9 \u2014 \u539F\u6587\u5DF2\u586B\u56DE\u8F93\u5165\u6846"
3777
+ },
3626
3778
  sessionPicker: {
3627
3779
  header: " \u25C8 REASONIX \xB7 \u9009\u62E9\u4F1A\u8BDD ",
3628
3780
  title: "\u9009\u62E9\u4F1A\u8BDD \u2014 {workspace}",
@@ -3756,6 +3908,11 @@ var zhCN = {
3756
3908
  metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE",
3757
3909
  metasoParseError: "web_search: Metaso \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3758
3910
  metasoApiError: "web_search: Metaso API \u9519\u8BEF\uFF08code {code}: {message}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3911
+ tavilyMissingKey: "web_search: Tavily \u540E\u7AEF\u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E TAVILY_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 ~/.reasonix/config.json \u4E2D\u914D\u7F6E `tavilyApiKey`\uFF1Bhttps://tavily.com \u6BCF\u6708 1000 \u6B21\u514D\u8D39",
3912
+ tavilyUnauthorized: "web_search: Tavily API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 TAVILY_API_KEY\uFF0C\u6216\u5728 https://tavily.com \u83B7\u53D6\u5BC6\u94A5",
3913
+ tavilyRateLimit: "web_search: Tavily \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u3001\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
3914
+ tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE",
3915
+ tavilyParseError: "web_search: Tavily \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3759
3916
  fetchStatus: "web_fetch {status} for {url} \u2014 try: \u5728\u6D4F\u89C8\u5668\u4E2D\u786E\u8BA4\u8BE5 URL \u80FD\u5426\u8BBF\u95EE\uFF1B\u8BE5\u72B6\u6001\u7801\u8868\u660E\u76EE\u6807\u4E3B\u673A\u8FD4\u56DE\u4E86\u9519\u8BEF\u9875\u9762",
3760
3917
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: \u7B49\u5F85 10 \u79D2\u540E\u91CD\u8BD5\uFF1B\u76EE\u6807\u4E3B\u673A\u6B63\u5728\u5BF9\u8BE5\u5BA2\u6237\u7AEF\u8FDB\u884C\u9650\u6D41",
3761
3918
  fetchForbidden403: "web_fetch 403 for {url} \u2014 try: \u76EE\u6807\u4E3B\u673A\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u8BE5\u9875\u9762\u53EF\u80FD\u9700\u8981\u767B\u5F55\u6216\u5C4F\u853D\u722C\u866B \u2014 \u6539\u7528 web_search \u6458\u8981",
@@ -3865,7 +4022,7 @@ var zhCN = {
3865
4022
  slow: "\u7F13\u6162 \xB7 {ms}ms",
3866
4023
  verySlow: "\u975E\u5E38\u6162 \xB7 {ms}ms",
3867
4024
  slowToast: "\u26A0 MCP `{name}` \u54CD\u5E94\u7F13\u6162 \xB7 P95 {seconds}s \xB7 \u6700\u8FD1 {sampleSize} \u6B21\u8C03\u7528",
3868
- emptyHint: "\u2139 \u672A\u914D\u7F6E MCP \u670D\u52A1\u5668 \u2014\u2014 \u53EF\u5C1D\u8BD5\uFF1A`reasonix setup` \u91CD\u65B0\u9009\u62E9\uFF0C\u6216 `reasonix mcp install filesystem`"
4025
+ emptyHint: "\u2139 \u672A\u914D\u7F6E MCP \u670D\u52A1\u5668 \u2014\u2014 \u53EF\u5C1D\u8BD5\uFF1A`reasonix setup` \u91CD\u65B0\u9009\u62E9\uFF0C\u6216 `reasonix mcp install filesystem` \xB7 shell \u547D\u4EE4\u6309\u6B21\u5BA1\u6279\uFF08allow once / allow always / deny\uFF09\uFF0C\u65E0\u5168\u5C40\u653E\u884C"
3869
4026
  },
3870
4027
  denyContextInput: {
3871
4028
  description: "\u544A\u8BC9\u6A21\u578B\u4F60\u4E3A\u4EC0\u4E48\u62D2\u7EDD\u4E86\u3002\u6A21\u578B\u4E0B\u6B21\u4F1A\u770B\u5230\u4F60\u7684\u7406\u7531\u4F5C\u4E3A\u989D\u5916\u7684\u4E0A\u4E0B\u6587\u3002"
@@ -3874,7 +4031,8 @@ var zhCN = {
3874
4031
  scrollAbove: " \u2191 {scroll}/{max} \u884C",
3875
4032
  scrollAbovePlural: " \u2191 {scroll}/{max} \u884C",
3876
4033
  scrollMore: " \u2014 \u8FD8\u6709 {remaining} \u884C",
3877
- scrollPgUp: " \xB7 PgUp/\u6EDA\u8F6E/\u2191"
4034
+ scrollPgUp: " \xB7 PgUp/\u6EDA\u8F6E",
4035
+ scrollCopy: " \xB7 /copy \u8FDB\u5165\u590D\u5236\u6A21\u5F0F"
3878
4036
  },
3879
4037
  slashArgPicker: {
3880
4038
  noMatch: '\u6CA1\u6709\u5339\u914D "{partial}"',
@@ -3932,7 +4090,8 @@ var zhCN = {
3932
4090
  reconnectDetail: "\u65AD\u5F00\u65E7\u8FDE\u63A5 \xB7 \u91CD\u65B0\u63E1\u624B \xB7 \u5217\u51FA\u5DE5\u5177",
3933
4091
  disabledDetail: "\u901A\u8FC7 /mcp disable {name}",
3934
4092
  failedSetupHint: "\u2192 \u8FD0\u884C `reasonix setup` \u79FB\u9664\u6B64\u6761\u76EE\uFF0C\u6216\u4FEE\u590D\u5E95\u5C42\u95EE\u9898\uFF08\u7F3A\u5C11 npm \u5305\u3001\u7F51\u7EDC\u7B49\uFF09\u3002",
3935
- failedSetupConfigHint: "\u2192 \u8FD0\u884C `reasonix setup` \u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u4E2D\u79FB\u9664\u635F\u574F\u7684\u6761\u76EE\u3002"
4093
+ failedSetupConfigHint: "\u2192 \u8FD0\u884C `reasonix setup` \u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u4E2D\u79FB\u9664\u635F\u574F\u7684\u6761\u76EE\u3002",
4094
+ abortedHint: "\u5DF2\u4E2D\u65AD MCP \u542F\u52A8 \u2014 \u8DF3\u8FC7 {count} \u4E2A\u670D\u52A1\u5668\u3002\u95EE\u9898\u4FEE\u590D\u540E\u7528 /mcp \u91CD\u65B0\u8FDE\u63A5\u3002"
3936
4095
  },
3937
4096
  checkpointPicker: {
3938
4097
  title: "\u6062\u590D\u68C0\u67E5\u70B9 \u2014 {workspace}",
@@ -4662,10 +4821,13 @@ var ToolRegistry = class {
4662
4821
  _autoFlatten;
4663
4822
  _planMode = false;
4664
4823
  _interceptor = null;
4824
+ _interceptors = [];
4665
4825
  _auditListener = null;
4666
4826
  _resultAugmenter = null;
4667
4827
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
4668
4828
  _lastMalformed = /* @__PURE__ */ new Map();
4829
+ /** Per-tool fingerprint of the last host-side interceptor rejection. */
4830
+ _lastInterceptorRejection = /* @__PURE__ */ new Map();
4669
4831
  constructor(opts = {}) {
4670
4832
  this._autoFlatten = opts.autoFlatten !== false;
4671
4833
  }
@@ -4681,6 +4843,18 @@ var ToolRegistry = class {
4681
4843
  setToolInterceptor(fn) {
4682
4844
  this._interceptor = fn;
4683
4845
  }
4846
+ /** Ordered host-side interceptors. They run before the legacy single interceptor. */
4847
+ addToolInterceptor(id, fn) {
4848
+ const normalized = id.trim();
4849
+ if (!normalized) throw new Error("tool interceptor requires a non-empty id");
4850
+ const existing = this._interceptors.findIndex((entry) => entry.id === normalized);
4851
+ if (existing >= 0) this._interceptors.splice(existing, 1);
4852
+ this._interceptors.push({ id: normalized, fn });
4853
+ return () => {
4854
+ const idx = this._interceptors.findIndex((entry) => entry.id === normalized);
4855
+ if (idx >= 0) this._interceptors.splice(idx, 1);
4856
+ };
4857
+ }
4684
4858
  setAuditListener(fn) {
4685
4859
  this._auditListener = fn;
4686
4860
  }
@@ -4769,16 +4943,27 @@ var ToolRegistry = class {
4769
4943
  rejectedReason: "plan-mode"
4770
4944
  });
4771
4945
  }
4772
- if (this._interceptor) {
4946
+ const chain = this._interceptor ? [...this._interceptors.map((entry) => entry.fn), this._interceptor] : this._interceptors.map((entry) => entry.fn);
4947
+ for (const interceptor of chain) {
4773
4948
  try {
4774
- const short = await this._interceptor(name, args);
4775
- if (typeof short === "string") return short;
4949
+ const short = await interceptor(name, args);
4950
+ if (typeof short === "string") {
4951
+ const guarded = this._noteInterceptorRejection(name, fingerprint, short);
4952
+ return this._augmentResult(name, args, guarded);
4953
+ }
4776
4954
  } catch (err) {
4777
4955
  return JSON.stringify({
4778
4956
  error: `${name}: interceptor failed \u2014 ${err.message}`
4779
4957
  });
4780
4958
  }
4781
4959
  }
4960
+ this._lastInterceptorRejection.delete(name);
4961
+ if (opts.signal?.aborted) {
4962
+ return JSON.stringify({
4963
+ error: `${name}: aborted before dispatch (user interrupt)`,
4964
+ rejectedReason: "aborted"
4965
+ });
4966
+ }
4782
4967
  let finalResult;
4783
4968
  try {
4784
4969
  try {
@@ -4810,13 +4995,16 @@ var ToolRegistry = class {
4810
4995
  finalResult = JSON.stringify({ error: `${e.name}: ${e.message}` });
4811
4996
  }
4812
4997
  }
4998
+ return this._augmentResult(name, args, finalResult);
4999
+ }
5000
+ _augmentResult(name, args, result) {
4813
5001
  if (this._resultAugmenter) {
4814
5002
  try {
4815
- return this._resultAugmenter(name, args, finalResult);
5003
+ return this._resultAugmenter(name, args, result);
4816
5004
  } catch {
4817
5005
  }
4818
5006
  }
4819
- return finalResult;
5007
+ return result;
4820
5008
  }
4821
5009
  /** 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. */
4822
5010
  _noteMalformed(name, fingerprint, detail) {
@@ -4830,7 +5018,35 @@ var ToolRegistry = class {
4830
5018
  }
4831
5019
  return JSON.stringify({ error: `${name}: ${detail}` });
4832
5020
  }
5021
+ _noteInterceptorRejection(name, fingerprint, result) {
5022
+ const reason = rejectedReason(result);
5023
+ if (!reason) {
5024
+ this._lastInterceptorRejection.delete(name);
5025
+ return result;
5026
+ }
5027
+ const key = `${reason}:${fingerprint}`;
5028
+ const prev = this._lastInterceptorRejection.get(name);
5029
+ this._lastInterceptorRejection.set(name, key);
5030
+ if (prev === key) {
5031
+ return JSON.stringify({
5032
+ 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.`,
5033
+ rejectedReason: reason,
5034
+ consecutiveInterceptorRejection: true
5035
+ });
5036
+ }
5037
+ return result;
5038
+ }
4833
5039
  };
5040
+ function rejectedReason(result) {
5041
+ try {
5042
+ const parsed = JSON.parse(result);
5043
+ if (!parsed || typeof parsed !== "object") return null;
5044
+ const reason = parsed.rejectedReason;
5045
+ return typeof reason === "string" && reason ? reason : null;
5046
+ } catch {
5047
+ return null;
5048
+ }
5049
+ }
4834
5050
  function isReadOnlyCall(tool, args) {
4835
5051
  if (tool.readOnlyCheck) {
4836
5052
  try {
@@ -5011,11 +5227,42 @@ async function bridgeMcpTools(client, opts = {}) {
5011
5227
  return { ...result, env };
5012
5228
  }
5013
5229
  function flattenMcpResult(result, opts = {}) {
5230
+ validateResultShape(result);
5014
5231
  const parts = result.content.map(blockToString);
5015
5232
  const joined = parts.join("\n").trim();
5016
5233
  const prefixed = result.isError ? `ERROR: ${joined || "(no error message from server)"}` : joined;
5017
5234
  return opts.maxChars ? truncateForModel(prefixed, opts.maxChars) : prefixed;
5018
5235
  }
5236
+ function validateResultShape(result) {
5237
+ if (typeof result !== "object" || !result)
5238
+ throw new Error(`MCP server returned non-object result: ${typeof result}`);
5239
+ const { content, isError: _isError } = result;
5240
+ if (!Array.isArray(content))
5241
+ throw new Error(`MCP server returned result with non-array content: ${typeof content}`);
5242
+ for (let i = 0; i < content.length; i++) {
5243
+ const block = content[i];
5244
+ if (typeof block !== "object" || !block)
5245
+ throw new Error(`MCP server returned result.content[${i}] is not an object`);
5246
+ if (block.type !== "text" && block.type !== "image")
5247
+ throw new Error(
5248
+ `MCP server returned result.content[${i}] with unknown type ${JSON.stringify(block.type)}`
5249
+ );
5250
+ if (block.type === "text" && typeof block.text !== "string")
5251
+ throw new Error(
5252
+ `MCP server returned result.content[${i}] with non-string text (${typeof block.text})`
5253
+ );
5254
+ if (block.type === "image") {
5255
+ if (typeof block.data !== "string")
5256
+ throw new Error(
5257
+ `MCP server returned result.content[${i}] with non-string data (${typeof block.data})`
5258
+ );
5259
+ if (typeof block.mimeType !== "string")
5260
+ throw new Error(
5261
+ `MCP server returned result.content[${i}] with non-string mimeType (${typeof block.mimeType})`
5262
+ );
5263
+ }
5264
+ }
5265
+ }
5019
5266
  function truncateForModel(s, maxChars) {
5020
5267
  if (s.length <= maxChars) return s;
5021
5268
  const tailBudget = Math.min(1024, Math.floor(maxChars * 0.1));
@@ -5198,31 +5445,38 @@ function appendSessionMessage(name, message) {
5198
5445
  } catch {
5199
5446
  }
5200
5447
  }
5201
- function listSessions() {
5448
+ function listSessions(opts) {
5202
5449
  const dir = sessionsDir();
5203
5450
  if (!existsSync3(dir)) return [];
5451
+ const want = opts?.workspaceFilter ? normalizeWorkspace(opts.workspaceFilter) : null;
5204
5452
  try {
5205
5453
  const files = readdirSync(dir).filter(
5206
5454
  (f) => f.endsWith(".jsonl") && !f.endsWith(".events.jsonl")
5207
5455
  );
5208
- return files.map((file) => {
5456
+ return files.flatMap((file) => {
5209
5457
  const path2 = join4(dir, file);
5210
- const stat2 = statSync(path2);
5211
5458
  const name = file.replace(/\.jsonl$/, "");
5459
+ const meta = loadSessionMeta(name);
5460
+ if (want !== null) {
5461
+ if (typeof meta.workspace !== "string") return [];
5462
+ if (normalizeWorkspace(meta.workspace) !== want) return [];
5463
+ }
5464
+ const stat2 = statSync(path2);
5212
5465
  const messageCount = countLines(path2);
5213
- return {
5214
- name,
5215
- path: path2,
5216
- size: stat2.size,
5217
- messageCount,
5218
- mtime: stat2.mtime,
5219
- meta: loadSessionMeta(name)
5220
- };
5466
+ return [{ name, path: path2, size: stat2.size, messageCount, mtime: stat2.mtime, meta }];
5221
5467
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
5222
5468
  } catch {
5223
5469
  return [];
5224
5470
  }
5225
5471
  }
5472
+ function normalizeWorkspace(p, platform = process.platform) {
5473
+ if (typeof p !== "string" || p.length === 0) return "";
5474
+ if (platform === "win32") {
5475
+ const resolved = win32Path.resolve(p);
5476
+ return resolved.replace(/\\/g, "/").replace(/^([A-Z]):/i, (_, d) => `${d.toLowerCase()}:`);
5477
+ }
5478
+ return posixPath.resolve(p);
5479
+ }
5226
5480
  function metaPath(name) {
5227
5481
  return join4(sessionsDir(), `${sanitizeName(name)}.meta.json`);
5228
5482
  }
@@ -5312,8 +5566,13 @@ function archiveSession(name) {
5312
5566
  }
5313
5567
  function countLines(path2) {
5314
5568
  try {
5315
- const raw = readFileSync4(path2, "utf8");
5316
- return raw.split(/\r?\n/).filter((l) => l.trim()).length;
5569
+ const buf = readFileSync4(path2);
5570
+ let count = 0;
5571
+ for (let i = 0; i < buf.length; i++) {
5572
+ if (buf[i] === 10) count++;
5573
+ }
5574
+ if (buf.length > 0 && buf[buf.length - 1] !== 10) count++;
5575
+ return count;
5317
5576
  } catch {
5318
5577
  return 0;
5319
5578
  }
@@ -6834,6 +7093,32 @@ ${reason}`
6834
7093
  }
6835
7094
  return userText;
6836
7095
  }
7096
+ /** Rewind to the N-th user turn (0-indexed). Drops that turn + everything after. */
7097
+ rewindToUserTurn(userTurnIndex) {
7098
+ const entries = this.log.entries;
7099
+ let count = 0;
7100
+ let targetIdx = -1;
7101
+ for (let i = 0; i < entries.length; i++) {
7102
+ if (entries[i].role !== "user") continue;
7103
+ if (count === userTurnIndex) {
7104
+ targetIdx = i;
7105
+ break;
7106
+ }
7107
+ count++;
7108
+ }
7109
+ if (targetIdx < 0) return null;
7110
+ const raw = entries[targetIdx].content;
7111
+ const userText = typeof raw === "string" ? raw : "";
7112
+ const preserved = entries.slice(0, targetIdx).map((m) => ({ ...m }));
7113
+ this.log.compactInPlace(preserved);
7114
+ if (this.sessionName) {
7115
+ try {
7116
+ rewriteSession(this.sessionName, preserved);
7117
+ } catch {
7118
+ }
7119
+ }
7120
+ return userText;
7121
+ }
6837
7122
  async *step(userInput) {
6838
7123
  this._steerConsumed = false;
6839
7124
  if (this.budgetUsd !== null) {
@@ -8001,10 +8286,15 @@ var SkillStore = class {
8001
8286
  dir: join7(this.projectRoot, ".agents", SKILLS_DIRNAME),
8002
8287
  scope: "project"
8003
8288
  });
8289
+ out.push({
8290
+ dir: join7(this.projectRoot, ".claude", SKILLS_DIRNAME),
8291
+ scope: "project"
8292
+ });
8004
8293
  }
8005
8294
  for (const dir of this.customSkillPaths) out.push({ dir, scope: "custom" });
8006
8295
  out.push({ dir: join7(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
8007
8296
  out.push({ dir: join7(this.homeDir, ".agents", SKILLS_DIRNAME), scope: "global" });
8297
+ out.push({ dir: join7(this.homeDir, ".claude", SKILLS_DIRNAME), scope: "global" });
8008
8298
  return out.map((root, priority) => ({ ...root, priority, status: skillPathStatus(root.dir) }));
8009
8299
  }
8010
8300
  customRoots() {
@@ -8107,14 +8397,20 @@ var SkillStore = class {
8107
8397
  }
8108
8398
  const { data, body } = parseFrontmatter(raw);
8109
8399
  const name = data.name && isValidSkillName(data.name) ? data.name : stem;
8400
+ const description = (data.description ?? "").trim();
8401
+ if (!description) {
8402
+ console.warn(
8403
+ `[skills] "${name}" at ${path2} has no description: \u2014 it will be loaded but won't appear in the skills index.`
8404
+ );
8405
+ }
8110
8406
  return {
8111
8407
  name,
8112
- description: (data.description ?? "").trim(),
8408
+ description,
8113
8409
  body: body.trim(),
8114
8410
  scope,
8115
8411
  path: path2,
8116
8412
  allowedTools: parseAllowedTools(data["allowed-tools"]),
8117
- runAs: parseRunAs(data.runAs),
8413
+ runAs: parseRunAs(data.runAs, data.context, data.agent),
8118
8414
  model: data.model?.startsWith("deepseek-") ? data.model : void 0
8119
8415
  };
8120
8416
  }
@@ -8147,8 +8443,11 @@ function skillPathStatus(dir) {
8147
8443
  return "unreadable";
8148
8444
  }
8149
8445
  }
8150
- function parseRunAs(raw) {
8151
- return raw?.trim() === "subagent" ? "subagent" : "inline";
8446
+ function parseRunAs(raw, context, agent) {
8447
+ if (raw?.trim() === "subagent") return "subagent";
8448
+ if (context?.trim().toLowerCase() === "fork") return "subagent";
8449
+ if (agent?.trim()) return "subagent";
8450
+ return "inline";
8152
8451
  }
8153
8452
  function skillStubBody(name) {
8154
8453
  return `---
@@ -8710,13 +9009,13 @@ import picomatch3 from "picomatch";
8710
9009
  // src/memory/subdir.ts
8711
9010
  import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
8712
9011
  import { dirname as dirname5, join as join9, relative as relative2, resolve as resolve5 } from "path";
8713
- function findSubdirMemoryAncestors(absPath, rootDir) {
9012
+ function findDirMemory(absDir, rootDir) {
8714
9013
  const root = resolve5(rootDir);
8715
- const target = resolve5(absPath);
9014
+ const target = resolve5(absDir);
8716
9015
  const rel = relative2(root, target);
8717
- if (!rel || rel.startsWith("..")) return [];
9016
+ if (rel.startsWith("..")) return [];
8718
9017
  const found = [];
8719
- let cur = dirname5(target);
9018
+ let cur = target;
8720
9019
  while (cur !== root) {
8721
9020
  const r = relative2(root, cur);
8722
9021
  if (!r || r.startsWith("..")) break;
@@ -8733,6 +9032,9 @@ function findSubdirMemoryAncestors(absPath, rootDir) {
8733
9032
  }
8734
9033
  return found;
8735
9034
  }
9035
+ function findSubdirMemoryAncestors(absPath, rootDir) {
9036
+ return findDirMemory(dirname5(resolve5(absPath)), rootDir);
9037
+ }
8736
9038
  function readSubdirMemoryContent(path2) {
8737
9039
  let raw;
8738
9040
  try {
@@ -8820,7 +9122,7 @@ async function applyMultiEdit(rootDir, edits) {
8820
9122
  );
8821
9123
  }
8822
9124
  const le = before.includes("\r\n") ? "\r\n" : "\n";
8823
- state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
9125
+ state = { before, buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
8824
9126
  filesByPath.set(e.abs, state);
8825
9127
  }
8826
9128
  const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
@@ -8828,7 +9130,7 @@ async function applyMultiEdit(rootDir, edits) {
8828
9130
  const firstIdx = state.buf.indexOf(adaptedSearch);
8829
9131
  if (firstIdx < 0) {
8830
9132
  throw new Error(
8831
- `multi_edit: edit #${i + 1} search text not found in ${rel} \u2014 no edits applied (multi_edit is atomic)`
9133
+ `multi_edit: edit #${i + 1} search text not found in ${rel} \u2014 no edits applied`
8832
9134
  );
8833
9135
  }
8834
9136
  const nextIdx = state.buf.indexOf(adaptedSearch, firstIdx + 1);
@@ -8844,8 +9146,29 @@ ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
8844
9146
  state.deltaChars += adaptedReplace.length - adaptedSearch.length;
8845
9147
  state.touched++;
8846
9148
  }
8847
- for (const [abs, state] of filesByPath) {
8848
- await fs.writeFile(abs, state.buf, "utf8");
9149
+ const attempted = [];
9150
+ try {
9151
+ for (const [abs, state] of filesByPath) {
9152
+ attempted.push({ abs, before: state.before });
9153
+ await fs.writeFile(abs, state.buf, "utf8");
9154
+ }
9155
+ } catch (writeErr) {
9156
+ const rollbackFailures = [];
9157
+ for (const item of [...attempted].reverse()) {
9158
+ try {
9159
+ await fs.writeFile(item.abs, item.before, "utf8");
9160
+ } catch (restoreErr) {
9161
+ rollbackFailures.push(`${displayRel(rootDir, item.abs)}: ${restoreErr.message}`);
9162
+ }
9163
+ }
9164
+ if (rollbackFailures.length > 0) {
9165
+ throw new Error(
9166
+ `multi_edit: write failed after partial application: ${writeErr.message}; rollback failed for ${rollbackFailures.join("; ")}`
9167
+ );
9168
+ }
9169
+ throw new Error(
9170
+ `multi_edit: write failed: ${writeErr.message}; rolled back all files that may have been modified`
9171
+ );
8849
9172
  }
8850
9173
  const fileCount = filesByPath.size;
8851
9174
  const editCount = edits.length;
@@ -9157,6 +9480,129 @@ function formatOutline(entries) {
9157
9480
  // src/tools/fs/search.ts
9158
9481
  import { promises as fs3 } from "fs";
9159
9482
  import * as pathMod4 from "path";
9483
+
9484
+ // src/tools/fs/regex-runner.ts
9485
+ import { Worker } from "worker_threads";
9486
+ var WORKER_SOURCE = `
9487
+ const { parentPort } = require("node:worker_threads");
9488
+ parentPort.on("message", (msg) => {
9489
+ const { id, text, source, flags } = msg;
9490
+ let re;
9491
+ try {
9492
+ re = new RegExp(source, flags);
9493
+ } catch (err) {
9494
+ parentPort.postMessage({ id, error: (err && err.message) ? err.message : String(err) });
9495
+ return;
9496
+ }
9497
+ const lines = text.split(/\\r?\\n/);
9498
+ const hits = [];
9499
+ for (let i = 0; i < lines.length; i++) {
9500
+ if (re.test(lines[i])) hits.push(i);
9501
+ }
9502
+ parentPort.postMessage({ id, hits });
9503
+ });
9504
+ `;
9505
+ var DEFAULT_TIMEOUT_MS = 6e4;
9506
+ var RegexRunner = class {
9507
+ worker = null;
9508
+ pending = /* @__PURE__ */ new Map();
9509
+ nextId = 1;
9510
+ defaultTimeoutMs;
9511
+ constructor(opts = {}) {
9512
+ this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
9513
+ }
9514
+ testLines(text, source, flags, opts = {}) {
9515
+ return new Promise((resolve13, reject) => {
9516
+ if (opts.signal?.aborted) {
9517
+ reject(new Error("regex evaluation aborted"));
9518
+ return;
9519
+ }
9520
+ if (!this.worker) this.worker = this.spawn();
9521
+ const id = this.nextId++;
9522
+ const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
9523
+ const timer = setTimeout(() => {
9524
+ this.pending.delete(id);
9525
+ this.killWorker();
9526
+ reject(new Error(`regex evaluation exceeded ${timeoutMs}ms`));
9527
+ }, timeoutMs);
9528
+ const entry = { resolve: resolve13, reject, timer };
9529
+ if (opts.signal) {
9530
+ entry.signal = opts.signal;
9531
+ entry.onAbort = () => {
9532
+ this.pending.delete(id);
9533
+ clearTimeout(timer);
9534
+ this.killWorker();
9535
+ reject(new Error("regex evaluation aborted"));
9536
+ };
9537
+ opts.signal.addEventListener("abort", entry.onAbort, { once: true });
9538
+ }
9539
+ this.pending.set(id, entry);
9540
+ this.worker.postMessage({ id, text, source, flags });
9541
+ });
9542
+ }
9543
+ async shutdown() {
9544
+ if (this.worker) {
9545
+ const w = this.worker;
9546
+ this.worker = null;
9547
+ await w.terminate();
9548
+ }
9549
+ for (const entry of this.pending.values()) {
9550
+ clearTimeout(entry.timer);
9551
+ if (entry.onAbort && entry.signal) {
9552
+ entry.signal.removeEventListener("abort", entry.onAbort);
9553
+ }
9554
+ entry.reject(new Error("regex runner shut down"));
9555
+ }
9556
+ this.pending.clear();
9557
+ }
9558
+ spawn() {
9559
+ const w = new Worker(WORKER_SOURCE, { eval: true });
9560
+ w.on("message", (msg) => {
9561
+ const entry = this.pending.get(msg.id);
9562
+ if (!entry) return;
9563
+ clearTimeout(entry.timer);
9564
+ if (entry.onAbort && entry.signal) {
9565
+ entry.signal.removeEventListener("abort", entry.onAbort);
9566
+ }
9567
+ this.pending.delete(msg.id);
9568
+ if (msg.error !== void 0) entry.reject(new Error(msg.error));
9569
+ else entry.resolve(msg.hits ?? []);
9570
+ });
9571
+ w.on("error", (err) => {
9572
+ if (this.worker !== w) return;
9573
+ this.failPending(err);
9574
+ });
9575
+ w.on("exit", () => {
9576
+ if (this.worker !== w) return;
9577
+ this.worker = null;
9578
+ if (this.pending.size > 0) this.failPending(new Error("regex worker exited"));
9579
+ });
9580
+ return w;
9581
+ }
9582
+ killWorker() {
9583
+ if (!this.worker) return;
9584
+ const w = this.worker;
9585
+ this.worker = null;
9586
+ void w.terminate();
9587
+ }
9588
+ failPending(err) {
9589
+ for (const entry of this.pending.values()) {
9590
+ clearTimeout(entry.timer);
9591
+ if (entry.onAbort && entry.signal) {
9592
+ entry.signal.removeEventListener("abort", entry.onAbort);
9593
+ }
9594
+ entry.reject(err);
9595
+ }
9596
+ this.pending.clear();
9597
+ }
9598
+ };
9599
+ var _runner = null;
9600
+ function getRegexRunner() {
9601
+ if (!_runner) _runner = new RegexRunner();
9602
+ return _runner;
9603
+ }
9604
+
9605
+ // src/tools/fs/search.ts
9160
9606
  function throwIfAborted(signal) {
9161
9607
  if (!signal?.aborted) return;
9162
9608
  throw new DOMException("search aborted by user", "AbortError");
@@ -9209,17 +9655,20 @@ async function searchFiles(ctx, startAbs, args) {
9209
9655
  }
9210
9656
  var MAX_HITS_PER_FILE = 30;
9211
9657
  var SUMMARY_MODE_TRIGGER_RATIO = 0.8;
9658
+ var WALK_DEADLINE_MS = 12e4;
9212
9659
  async function searchContent(ctx, startAbs, args) {
9213
9660
  throwIfAborted(args.signal);
9214
9661
  const caseSensitive = args.case_sensitive === true;
9215
9662
  const includeDeps = args.include_deps === true;
9216
9663
  const ctxLines = Math.max(0, Math.min(20, Math.floor(args.context ?? 0)));
9217
9664
  const summaryOnly = args.summary_only === true;
9218
- let re = null;
9665
+ const reFlags = caseSensitive ? "" : "i";
9666
+ let reSource = null;
9219
9667
  try {
9220
- re = new RegExp(args.pattern, caseSensitive ? "" : "i");
9668
+ new RegExp(args.pattern, reFlags);
9669
+ reSource = args.pattern;
9221
9670
  } catch {
9222
- re = null;
9671
+ reSource = null;
9223
9672
  }
9224
9673
  const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
9225
9674
  const matches = [];
@@ -9229,6 +9678,15 @@ async function searchContent(ctx, startAbs, args) {
9229
9678
  let summaryMode = summaryOnly;
9230
9679
  let summaryNoticeEmitted = false;
9231
9680
  const fileHitCounts = /* @__PURE__ */ new Map();
9681
+ const regexSkippedFiles = [];
9682
+ const t0 = Date.now();
9683
+ const throwIfTimedOut = () => {
9684
+ if (Date.now() - t0 > WALK_DEADLINE_MS) {
9685
+ throw new Error(
9686
+ `search_content exceeded ${WALK_DEADLINE_MS}ms \u2014 narrow the scope (path/glob) or simplify the pattern`
9687
+ );
9688
+ }
9689
+ };
9232
9690
  const pushLine = (out) => {
9233
9691
  if (totalBytes + out.length + 1 > ctx.maxListBytes) {
9234
9692
  matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
@@ -9263,6 +9721,7 @@ async function searchContent(ctx, startAbs, args) {
9263
9721
  for (const e of entries) {
9264
9722
  if (truncated) return;
9265
9723
  throwIfAborted(args.signal);
9724
+ throwIfTimedOut();
9266
9725
  if (e.isDirectory()) {
9267
9726
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
9268
9727
  await walk2(pathMod4.join(dir, e.name));
@@ -9299,13 +9758,25 @@ async function searchContent(ctx, startAbs, args) {
9299
9758
  const text = raw.toString("utf8");
9300
9759
  const rel = displayRel3(ctx.rootDir, full);
9301
9760
  const lines = text.split(/\r?\n/);
9302
- const hits = [];
9303
- for (let li = 0; li < lines.length; li++) {
9304
- throwIfAborted(args.signal);
9305
- const line = lines[li];
9306
- const lineForCheck = caseSensitive ? line : line.toLowerCase();
9307
- const hit = re ? re.test(line) : lineForCheck.includes(needle);
9308
- if (hit) hits.push(li);
9761
+ let hits;
9762
+ if (reSource !== null) {
9763
+ try {
9764
+ hits = await getRegexRunner().testLines(text, reSource, reFlags, {
9765
+ signal: args.signal
9766
+ });
9767
+ } catch (err) {
9768
+ const reason = err.message;
9769
+ if (reason.includes("aborted")) throw err;
9770
+ regexSkippedFiles.push({ rel, reason });
9771
+ continue;
9772
+ }
9773
+ } else {
9774
+ hits = [];
9775
+ for (let li = 0; li < lines.length; li++) {
9776
+ throwIfAborted(args.signal);
9777
+ const lineForCheck = caseSensitive ? lines[li] : lines[li].toLowerCase();
9778
+ if (lineForCheck.includes(needle)) hits.push(li);
9779
+ }
9309
9780
  }
9310
9781
  scanned++;
9311
9782
  if (hits.length === 0) continue;
@@ -9354,6 +9825,11 @@ async function searchContent(ctx, startAbs, args) {
9354
9825
  }
9355
9826
  };
9356
9827
  await walk2(startAbs);
9828
+ if (regexSkippedFiles.length > 0) {
9829
+ pushLine(
9830
+ `[regex timed out on ${regexSkippedFiles.length} file${regexSkippedFiles.length === 1 ? "" : "s"} \u2014 pattern may have catastrophic backtracking; first: ${regexSkippedFiles[0].rel}]`
9831
+ );
9832
+ }
9357
9833
  if (matches.length === 0) {
9358
9834
  return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
9359
9835
  }
@@ -9361,7 +9837,7 @@ async function searchContent(ctx, startAbs, args) {
9361
9837
  }
9362
9838
 
9363
9839
  // src/tools/filesystem.ts
9364
- var DEFAULT_OUTLINE_THRESHOLD_BYTES = 512 * 1024;
9840
+ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 64 * 1024;
9365
9841
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
9366
9842
  var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
9367
9843
  var OUTLINE_HEAD_LINES = 80;
@@ -9418,11 +9894,15 @@ function registerFilesystemTools(registry, opts) {
9418
9894
  const sessionApproved = /* @__PURE__ */ new Set();
9419
9895
  const shownSubdirMemory = /* @__PURE__ */ new Set();
9420
9896
  function withSubdirMemory(absPath, body) {
9421
- if (!memoryEnabled()) return body;
9422
- const ancestors = findSubdirMemoryAncestors(absPath, rootDir);
9423
- if (ancestors.length === 0) return body;
9897
+ return prependMemorySections(findSubdirMemoryAncestors(absPath, rootDir), body);
9898
+ }
9899
+ function withDirMemory(absDir, body) {
9900
+ return prependMemorySections(findDirMemory(absDir, rootDir), body);
9901
+ }
9902
+ function prependMemorySections(memPaths, body) {
9903
+ if (!memoryEnabled() || memPaths.length === 0) return body;
9424
9904
  const sections = [];
9425
- for (const memPath of [...ancestors].reverse()) {
9905
+ for (const memPath of [...memPaths].reverse()) {
9426
9906
  if (shownSubdirMemory.has(memPath)) continue;
9427
9907
  const content = readSubdirMemoryContent(memPath);
9428
9908
  if (!content) continue;
@@ -9499,11 +9979,7 @@ ${body}`;
9499
9979
  registry.register({
9500
9980
  name: "read_file",
9501
9981
  parallelSafe: true,
9502
- 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:
9503
- - head: N \u2192 first N lines (cheap probe of imports / config head)
9504
- - tail: N \u2192 last N lines (recent-tail of a log)
9505
- - range: "A-B" \u2192 inclusive 1-indexed range (e.g. "120-180" around an edit site)
9506
- 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.`,
9982
+ 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.`,
9507
9983
  readOnly: true,
9508
9984
  stormExempt: true,
9509
9985
  parameters: {
@@ -9615,17 +10091,13 @@ ${slice.join("\n")}`);
9615
10091
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
9616
10092
  lines.push(e.isDirectory() ? `${e.name}/` : e.name);
9617
10093
  }
9618
- return lines.join("\n") || "(empty directory)";
10094
+ return withDirMemory(abs, lines.join("\n") || "(empty directory)");
9619
10095
  }
9620
10096
  });
9621
10097
  registry.register({
9622
10098
  name: "directory_tree",
9623
10099
  parallelSafe: true,
9624
- description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
9625
- - 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.
9626
- - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
9627
- - 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.
9628
- Prefer \`list_directory\` for a single-level view, \`search_files\` to find specific paths, and \`search_content\` to find code.`,
10100
+ 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.`,
9629
10101
  readOnly: true,
9630
10102
  parameters: {
9631
10103
  type: "object",
@@ -9726,38 +10198,38 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
9726
10198
  registry.register({
9727
10199
  name: "search_content",
9728
10200
  parallelSafe: true,
9729
- 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.",
10201
+ 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.",
9730
10202
  readOnly: true,
9731
10203
  parameters: {
9732
10204
  type: "object",
9733
10205
  properties: {
9734
10206
  pattern: {
9735
10207
  type: "string",
9736
- description: "Substring (or regex) to search file contents for."
10208
+ description: "Substring or regex."
9737
10209
  },
9738
10210
  path: {
9739
10211
  type: "string",
9740
- description: "Directory to start the search at (default: sandbox root)."
10212
+ description: "Search root (default: sandbox root)."
9741
10213
  },
9742
10214
  glob: {
9743
10215
  type: "string",
9744
- 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."
10216
+ description: "Filename filter. Glob when it contains `*`/`?`/`{`/`[`; otherwise substring. Patterns with `/` match the path relative to the search root."
9745
10217
  },
9746
10218
  case_sensitive: {
9747
10219
  type: "boolean",
9748
- description: "When true, match case exactly. Default false (case-insensitive)."
10220
+ description: "Default false."
9749
10221
  },
9750
10222
  include_deps: {
9751
10223
  type: "boolean",
9752
- 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."
10224
+ description: "Also search node_modules / .git / dist / build / etc. Default off."
9753
10225
  },
9754
10226
  context: {
9755
10227
  type: "integer",
9756
- 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."
10228
+ description: "Lines of context around each match (both sides). Default 0, capped 20. Ripgrep-style output."
9757
10229
  },
9758
10230
  summary_only: {
9759
10231
  type: "boolean",
9760
- 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."
10232
+ description: "Skip line content, return `rel: N matches` per file. Use for 'where does this exist at all' before drilling in."
9761
10233
  }
9762
10234
  },
9763
10235
  required: ["pattern"]
@@ -9870,7 +10342,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
9870
10342
  });
9871
10343
  registry.register({
9872
10344
  name: "multi_edit",
9873
- 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.",
10345
+ 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.",
9874
10346
  parameters: {
9875
10347
  type: "object",
9876
10348
  properties: {
@@ -10027,7 +10499,7 @@ function registerMemoryTools(registry, opts = {}) {
10027
10499
  }
10028
10500
  registry.register({
10029
10501
  name: "remember",
10030
- 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.",
10502
+ 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.",
10031
10503
  parameters: {
10032
10504
  type: "object",
10033
10505
  properties: {
@@ -10038,29 +10510,29 @@ function registerMemoryTools(registry, opts = {}) {
10038
10510
  scope: {
10039
10511
  type: "string",
10040
10512
  enum: ["global", "project"],
10041
- description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
10513
+ description: "global = across all projects; project = current sandbox only (needs `reasonix code`)."
10042
10514
  },
10043
10515
  name: {
10044
10516
  type: "string",
10045
- description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
10517
+ description: "Filename-safe id, 3-40 chars, alnum + _ - . (no separators, no leading dot)."
10046
10518
  },
10047
10519
  description: {
10048
10520
  type: "string",
10049
- description: "One-line summary shown in MEMORY.md (under ~150 chars)."
10521
+ description: "\u2264150 char one-liner shown in MEMORY.md."
10050
10522
  },
10051
10523
  content: {
10052
10524
  type: "string",
10053
- description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
10525
+ description: "Markdown body. For feedback/project, structure as rule + **Why:** + **How to apply:**."
10054
10526
  },
10055
10527
  priority: {
10056
10528
  type: "string",
10057
10529
  enum: ["low", "medium", "high"],
10058
- 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."
10530
+ description: "`high` injects entry into HIGH PRIORITY block \u2014 use sparingly."
10059
10531
  },
10060
10532
  expires: {
10061
10533
  type: "string",
10062
10534
  enum: ["project_end"],
10063
- description: "Optional lifecycle hint. `project_end` causes `/memory clear project` to also remove this entry even when it's stored at global scope."
10535
+ description: "`project_end` lets /memory clear project remove this even at global scope."
10064
10536
  }
10065
10537
  },
10066
10538
  required: ["type", "scope", "name", "description", "content"]
@@ -10199,26 +10671,26 @@ function sanitizeOptions(raw) {
10199
10671
  function registerChoiceTool(registry, opts = {}) {
10200
10672
  registry.register({
10201
10673
  name: "ask_choice",
10202
- 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.",
10674
+ 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.",
10203
10675
  readOnly: true,
10204
10676
  parameters: {
10205
10677
  type: "object",
10206
10678
  properties: {
10207
10679
  question: {
10208
10680
  type: "string",
10209
- 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."
10681
+ description: "One-sentence question. Don't repeat the options here \u2014 the picker renders them."
10210
10682
  },
10211
10683
  options: {
10212
10684
  type: "array",
10213
- description: "2\u20134 alternatives. Each needs a stable id and a short title; summary is optional.",
10685
+ description: "2\u20136 alternatives. Each: stable id + short title; summary optional.",
10214
10686
  items: {
10215
10687
  type: "object",
10216
10688
  properties: {
10217
- id: { type: "string", description: "Short stable id (A, B, C, or option-1)." },
10218
- title: { type: "string", description: "One-line title shown as the option label." },
10689
+ id: { type: "string", description: "Stable id (A, B, C or option-1)." },
10690
+ title: { type: "string", description: "One-line label." },
10219
10691
  summary: {
10220
10692
  type: "string",
10221
- description: "Optional. A second dimmed line with more detail. Keep under ~80 chars."
10693
+ description: "Optional dimmed second line, \u226480 chars."
10222
10694
  }
10223
10695
  },
10224
10696
  required: ["id", "title"]
@@ -10226,7 +10698,7 @@ function registerChoiceTool(registry, opts = {}) {
10226
10698
  },
10227
10699
  allowCustom: {
10228
10700
  type: "boolean",
10229
- 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."
10701
+ description: "Shows a 'type my own answer' escape hatch. Default false."
10230
10702
  }
10231
10703
  },
10232
10704
  required: ["question", "options"]
@@ -10312,19 +10784,33 @@ var PlanRevisionProposedError = class extends Error {
10312
10784
  };
10313
10785
 
10314
10786
  // src/tools/plan-core.ts
10315
- 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.";
10316
- 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.";
10317
- 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.";
10787
+ 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.";
10788
+ 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.";
10789
+ 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.";
10318
10790
  var STEP_ITEM_SCHEMA = {
10319
10791
  type: "object",
10320
10792
  properties: {
10321
10793
  id: { type: "string", description: "Stable id, e.g. step-1." },
10322
10794
  title: { type: "string", description: "Short imperative title." },
10323
- action: { type: "string", description: "One-sentence description of the concrete action." },
10795
+ action: { type: "string", description: "One-sentence concrete action." },
10324
10796
  risk: {
10325
10797
  type: "string",
10326
10798
  enum: ["low", "med", "high"],
10327
- 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."
10799
+ description: "high = hard-to-undo / prod / API break; med = reversible multi-file; low = safe local. Omit if unsure."
10800
+ },
10801
+ targets: {
10802
+ type: "array",
10803
+ description: "Optional. Files/dirs/modules this step touches.",
10804
+ items: { type: "string" }
10805
+ },
10806
+ acceptance: {
10807
+ type: "string",
10808
+ description: "Optional. One-sentence completion criterion."
10809
+ },
10810
+ verification: {
10811
+ type: "array",
10812
+ description: "Optional. Verification commands/checks for this step.",
10813
+ items: { type: "string" }
10328
10814
  }
10329
10815
  },
10330
10816
  required: ["id", "title", "action"]
@@ -10346,10 +10832,42 @@ function sanitizeSteps(raw) {
10346
10832
  const step = { id, title, action };
10347
10833
  const risk = sanitizeRisk(e.risk);
10348
10834
  if (risk) step.risk = risk;
10835
+ const targets = sanitizeStringList(e.targets);
10836
+ if (targets) step.targets = targets;
10837
+ const acceptance = typeof e.acceptance === "string" ? e.acceptance.trim() : "";
10838
+ if (acceptance) step.acceptance = acceptance;
10839
+ const verification = sanitizeStringList(e.verification);
10840
+ if (verification) step.verification = verification;
10349
10841
  steps.push(step);
10350
10842
  }
10351
10843
  return steps.length > 0 ? steps : void 0;
10352
10844
  }
10845
+ function sanitizeStringList(raw) {
10846
+ if (!Array.isArray(raw)) return void 0;
10847
+ const out = raw.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
10848
+ return out.length > 0 ? out : void 0;
10849
+ }
10850
+ function sanitizeEvidence(raw) {
10851
+ if (!Array.isArray(raw)) return void 0;
10852
+ const out = [];
10853
+ for (const item of raw) {
10854
+ if (!item || typeof item !== "object") continue;
10855
+ const e = item;
10856
+ const kind = e.kind;
10857
+ if (kind !== "verification" && kind !== "diff" && kind !== "checkpoint" && kind !== "manual") {
10858
+ continue;
10859
+ }
10860
+ const summary = typeof e.summary === "string" ? e.summary.trim() : "";
10861
+ if (!summary) continue;
10862
+ const evidence = { kind, summary };
10863
+ const command = typeof e.command === "string" ? e.command.trim() : "";
10864
+ if (command) evidence.command = command;
10865
+ const paths = sanitizeStringList(e.paths);
10866
+ if (paths) evidence.paths = paths;
10867
+ out.push(evidence);
10868
+ }
10869
+ return out.length > 0 ? out : void 0;
10870
+ }
10353
10871
  function registerSubmitPlan(registry, opts) {
10354
10872
  registry.register({
10355
10873
  name: "submit_plan",
@@ -10360,16 +10878,16 @@ function registerSubmitPlan(registry, opts) {
10360
10878
  properties: {
10361
10879
  plan: {
10362
10880
  type: "string",
10363
- 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."
10881
+ description: "Markdown plan: one-line summary, file-by-file breakdown, risks/open questions."
10364
10882
  },
10365
10883
  steps: {
10366
10884
  type: "array",
10367
- 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.",
10885
+ description: "Structured step list \u2014 strongly recommended for >1 step. Stable ids (step-1, step-2, ...).",
10368
10886
  items: STEP_ITEM_SCHEMA
10369
10887
  },
10370
10888
  summary: {
10371
10889
  type: "string",
10372
- 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."
10890
+ description: "Optional ~80-char plan title for the picker header and /plans listings."
10373
10891
  }
10374
10892
  },
10375
10893
  required: ["plan"]
@@ -10407,19 +10925,33 @@ function registerMarkStepComplete(registry, opts) {
10407
10925
  properties: {
10408
10926
  stepId: {
10409
10927
  type: "string",
10410
- description: "The id of the step being marked complete. Must match one from submit_plan's steps array."
10928
+ description: "Step id from submit_plan's steps array."
10411
10929
  },
10412
10930
  title: {
10413
10931
  type: "string",
10414
- description: "Optional. The step's title, echoed back for the UI. If omitted, the UI falls back to the id."
10932
+ description: "Optional. Echoed for the UI; falls back to id."
10415
10933
  },
10416
10934
  result: {
10417
10935
  type: "string",
10418
- description: "One-sentence summary of what was done for this step."
10936
+ description: "One-sentence summary of what was done."
10419
10937
  },
10420
10938
  notes: {
10421
10939
  type: "string",
10422
- description: "Optional. Anything surprising \u2014 blockers hit, assumptions revised, follow-ups for later steps."
10940
+ description: "Optional. Surprises \u2014 blockers, revised assumptions, follow-ups."
10941
+ },
10942
+ evidence: {
10943
+ type: "array",
10944
+ description: "Optional. Verification summary / diff / checkpoint ref / manual note.",
10945
+ items: {
10946
+ type: "object",
10947
+ properties: {
10948
+ kind: { type: "string", enum: ["verification", "diff", "checkpoint", "manual"] },
10949
+ summary: { type: "string" },
10950
+ command: { type: "string" },
10951
+ paths: { type: "array", items: { type: "string" } }
10952
+ },
10953
+ required: ["kind", "summary"]
10954
+ }
10423
10955
  }
10424
10956
  },
10425
10957
  required: ["stepId", "result"]
@@ -10437,9 +10969,15 @@ function registerMarkStepComplete(registry, opts) {
10437
10969
  }
10438
10970
  const title = typeof args?.title === "string" ? args.title.trim() || void 0 : void 0;
10439
10971
  const notes = typeof args?.notes === "string" ? args.notes.trim() || void 0 : void 0;
10972
+ const evidence = sanitizeEvidence(args?.evidence);
10973
+ const evidenceReason = opts.requireStepEvidence?.({ stepId, title });
10974
+ if (evidenceReason && (!evidence || evidence.length === 0)) {
10975
+ throw new Error(`mark_step_complete: evidence required \u2014 ${evidenceReason}`);
10976
+ }
10440
10977
  const update = { kind: "step_completed", stepId, result };
10441
10978
  if (title) update.title = title;
10442
10979
  if (notes) update.notes = notes;
10980
+ if (evidence) update.evidence = evidence;
10443
10981
  opts.onStepCompleted?.(update);
10444
10982
  const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
10445
10983
  kind: "plan_checkpoint",
@@ -10464,16 +11002,16 @@ function registerRevisePlan(registry, opts) {
10464
11002
  properties: {
10465
11003
  reason: {
10466
11004
  type: "string",
10467
- description: "One sentence explaining why you're revising \u2014 what the user asked for, what changed your assessment."
11005
+ description: "One sentence \u2014 why you're revising / what the user asked for."
10468
11006
  },
10469
11007
  remainingSteps: {
10470
11008
  type: "array",
10471
- 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.",
11009
+ description: "New tail of the plan. Reuse old ids when adjusting; new ids for new steps.",
10472
11010
  items: STEP_ITEM_SCHEMA
10473
11011
  },
10474
11012
  summary: {
10475
11013
  type: "string",
10476
- description: "Optional. Updated one-line plan summary if the overall framing has shifted."
11014
+ description: "Optional. Updated one-line summary when framing has shifted."
10477
11015
  }
10478
11016
  },
10479
11017
  required: ["reason", "remainingSteps"]
@@ -10511,7 +11049,7 @@ function registerPlanTool(registry, opts = {}) {
10511
11049
  }
10512
11050
 
10513
11051
  // src/tools/todo.ts
10514
- 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.';
11052
+ 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.";
10515
11053
  function validateTodos(raw) {
10516
11054
  if (!Array.isArray(raw)) {
10517
11055
  throw new Error("todo_write: `todos` must be an array");
@@ -11902,8 +12440,13 @@ var OutputBuffer = class {
11902
12440
  };
11903
12441
 
11904
12442
  // src/tools/shell/parse.ts
11905
- import { homedir as homedir6 } from "os";
12443
+ import { homedir as homedir7 } from "os";
11906
12444
  import * as pathMod8 from "path";
12445
+
12446
+ // packages/core-utils/src/tildeify.ts
12447
+ import { homedir as homedir6 } from "os";
12448
+
12449
+ // src/tools/shell/parse.ts
11907
12450
  var BUILTIN_ALLOWLIST = [
11908
12451
  // Repo inspection
11909
12452
  "git status",
@@ -12103,12 +12646,12 @@ function resolveSensitivePath(token, projectRoot) {
12103
12646
  return null;
12104
12647
  let expanded = token;
12105
12648
  if (expanded.startsWith("~")) {
12106
- expanded = pathMod8.join(homedir6(), expanded.slice(1));
12649
+ expanded = pathMod8.join(homedir7(), expanded.slice(1));
12107
12650
  }
12108
12651
  return pathMod8.resolve(projectRoot, expanded);
12109
12652
  }
12110
12653
  function expandPrefix(prefix) {
12111
- if (prefix.startsWith("~")) return pathMod8.join(homedir6(), prefix.slice(1));
12654
+ if (prefix.startsWith("~")) return pathMod8.join(homedir7(), prefix.slice(1));
12112
12655
  return pathMod8.resolve(prefix);
12113
12656
  }
12114
12657
  function pathStartsWithPrefix(normalized, prefix) {
@@ -12481,7 +13024,7 @@ function registerShellTools(registry, opts) {
12481
13024
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
12482
13025
  registry.register({
12483
13026
  name: "run_command",
12484
- description: "Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; anything that could mutate state, install deps, or touch the network is gated by user confirmation. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.\n\nConstraints (no real shell \u2014 argv is parsed natively for cross-platform parity):\n\u2022 Supported: chain ops `|` / `||` / `&&` / `;` (each segment allowlist-checked individually), file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` (target paths resolve relative to project root, max one redirect per fd per segment).\n\u2022 NOT supported: background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, process substitution `<(\u2026)`, `$VAR` env expansion, glob expansion. To pass an operator char as literal arg, quote it (`grep \"a|b\" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain like `cd dir && cmd`. Use the binary's own cwd flag: `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>`, `pytest <dir>/tests`.\n\u2022 Filter at source \u2014 unbounded output (`netstat -ano`, `find /`) wastes tokens. Use `grep -c`, `wc -l`, narrower paths, etc.",
13027
+ description: 'Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; mutating / network / install commands gate on user confirmation.\n\nNo real shell \u2014 argv parsed natively for cross-platform parity:\n\u2022 Supported: chains `|`/`||`/`&&`/`;` (each segment allowlist-checked) and file redirects `>`/`>>`/`<`/`2>`/`2>>`/`2>&1`/`&>`.\n\u2022 Rejected: background `&`, heredoc `<<`, `$(\u2026)`, subshells, `$VAR` expansion, glob expansion. Quote operator chars as literals (`grep "a|b" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain. Use `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>` instead.\n\u2022 Filter at source \u2014 `grep -c` / `wc -l` / narrower paths over unbounded dumps.',
12485
13028
  // Plan-mode gate: allow allowlisted commands through (git status,
12486
13029
  // cargo check, ls, grep …) so the model can actually investigate
12487
13030
  // during planning. Anything that would otherwise trigger a
@@ -12536,7 +13079,7 @@ function registerShellTools(registry, opts) {
12536
13079
  });
12537
13080
  registry.register({
12538
13081
  name: "run_background",
12539
- description: "Spawn a long-running process and detach. Waits up to `waitSec` for startup or a readiness signal ('Local:', 'listening on', 'compiled successfully'), then returns the job id + startup preview. Tail logs with `job_output`, block on completion with `wait_for_job`, kill with `stop_job`, list with `list_jobs`.\n\nSingle process only \u2014 no chains / redirects. For subdirectories use the `cwd` parameter (workspace-relative or absolute, must stay inside the workspace root); do NOT write `cd X && cmd`, that gets rejected.\n\nUSE THIS \u2014 not run_command \u2014 for:\n- Dev servers / watchers: npm/yarn/pnpm dev, uvicorn / flask run, cargo watch, tsc --watch, webpack serve, anything with dev/serve/watch in the name.\n- One-shot long jobs: curl / wget large downloads, `huggingface-cli download`, multi-GB `pip install` / `npm install`, big `cargo build` / `docker build`. Start with `run_background`, then call `wait_for_job` once (default `waitFor: 'exit'`, timeoutMs up to 300_000) \u2014 the harness blocks server-side so a 5-minute download costs ONE tool call, not 30 polls.",
13082
+ description: "Spawn a long-running process and detach. Waits up to `waitSec` for startup or a readiness signal ('Local:', 'listening on', 'compiled successfully'), then returns job id + startup preview. Companion tools: `job_output`, `wait_for_job`, `stop_job`, `list_jobs`. Single process only \u2014 no chains/redirects. Use `cwd` (not `cd X && cmd`) for subdirs.\n\nUSE THIS \u2014 not run_command \u2014 for: dev servers / watchers (`npm dev`, `uvicorn`, `tsc --watch`, anything with dev/serve/watch in the name) AND one-shot long jobs (large `curl`, `pip install`, `cargo build`, `docker build`). Pair with `wait_for_job` for server-side blocking \u2014 one tool call regardless of duration.",
12540
13083
  parameters: {
12541
13084
  type: "object",
12542
13085
  properties: {
@@ -12743,6 +13286,7 @@ var FETCH_MAX_BYTES = 10 * 1024 * 1024;
12743
13286
  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";
12744
13287
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
12745
13288
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
13289
+ var TAVILY_ENDPOINT = "https://api.tavily.com/search";
12746
13290
  function searchStatusError(status) {
12747
13291
  if (status === 429) return t("webErrors.rateLimit429");
12748
13292
  if (status === 403) return t("webErrors.forbidden403");
@@ -12762,6 +13306,9 @@ async function webSearch(query, opts = {}) {
12762
13306
  if (opts.engine === "searxng") {
12763
13307
  return searchSearxng(query, opts);
12764
13308
  }
13309
+ if (opts.engine === "tavily") {
13310
+ return searchTavily(query, opts);
13311
+ }
12765
13312
  return searchMojeek(query, opts);
12766
13313
  }
12767
13314
  async function searchMojeek(query, opts = {}) {
@@ -12896,6 +13443,55 @@ async function searchMetaso(query, opts = {}) {
12896
13443
  snippet: wp.snippet ?? wp.summary ?? ""
12897
13444
  }));
12898
13445
  }
13446
+ async function searchTavily(query, opts = {}) {
13447
+ const topK = Math.max(1, Math.min(20, opts.topK ?? DEFAULT_TOPK));
13448
+ const apiKey = loadTavilyApiKey();
13449
+ if (!apiKey) throw new Error(t("webErrors.tavilyMissingKey"));
13450
+ let resp;
13451
+ try {
13452
+ resp = await fetch(TAVILY_ENDPOINT, {
13453
+ method: "POST",
13454
+ headers: {
13455
+ "Content-Type": "application/json",
13456
+ Accept: "application/json"
13457
+ },
13458
+ body: JSON.stringify({
13459
+ api_key: apiKey,
13460
+ query,
13461
+ search_depth: "basic",
13462
+ max_results: topK,
13463
+ include_answer: false,
13464
+ include_raw_content: false,
13465
+ include_images: false
13466
+ }),
13467
+ signal: opts.signal
13468
+ });
13469
+ } catch (err) {
13470
+ if (err instanceof TypeError && err.message.includes("fetch")) {
13471
+ throw new Error(t("webErrors.cannotReach", { endpoint: TAVILY_ENDPOINT }));
13472
+ }
13473
+ throw err;
13474
+ }
13475
+ if (!resp.ok) {
13476
+ if (resp.status === 401 || resp.status === 403) {
13477
+ throw new Error(t("webErrors.tavilyUnauthorized"));
13478
+ }
13479
+ if (resp.status === 429) throw new Error(t("webErrors.tavilyRateLimit"));
13480
+ throw new Error(t("webErrors.tavilyServerError", { status: resp.status }));
13481
+ }
13482
+ let data;
13483
+ try {
13484
+ data = await resp.json();
13485
+ } catch {
13486
+ throw new Error(t("webErrors.tavilyParseError", { status: resp.status }));
13487
+ }
13488
+ const results = data.results ?? [];
13489
+ return results.slice(0, topK).map((r) => ({
13490
+ title: r.title,
13491
+ url: r.url,
13492
+ snippet: r.content ?? ""
13493
+ }));
13494
+ }
12899
13495
  function parseSearxngHtmlResults(html) {
12900
13496
  const root = parseHtml(html);
12901
13497
  const results = [];
@@ -13114,7 +13710,7 @@ function registerWebTools(registry, opts = {}) {
13114
13710
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
13115
13711
  registry.register({
13116
13712
  name: "web_search",
13117
- 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.",
13713
+ 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.",
13118
13714
  readOnly: true,
13119
13715
  parallelSafe: true,
13120
13716
  parameters: {
@@ -13129,8 +13725,8 @@ function registerWebTools(registry, opts = {}) {
13129
13725
  required: ["query"]
13130
13726
  },
13131
13727
  fn: async (args, ctx) => {
13132
- const engine = opts.webSearchEngine ?? webSearchEngine();
13133
- const endpoint = opts.webSearchEndpoint ?? webSearchEndpoint();
13728
+ const engine = webSearchEngine();
13729
+ const endpoint = webSearchEndpoint();
13134
13730
  const results = await webSearch(args.query, {
13135
13731
  topK: args.topK ?? defaultTopK,
13136
13732
  signal: ctx?.signal,
@@ -13634,7 +14230,7 @@ function truncate(s, n) {
13634
14230
 
13635
14231
  // src/version.ts
13636
14232
  import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
13637
- import { homedir as homedir7 } from "os";
14233
+ import { homedir as homedir8 } from "os";
13638
14234
  import { dirname as dirname7, join as join14 } from "path";
13639
14235
  import { fileURLToPath as fileURLToPath2 } from "url";
13640
14236
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
@@ -13661,7 +14257,7 @@ function readPackageVersion() {
13661
14257
  }
13662
14258
  var VERSION = readPackageVersion();
13663
14259
  function cachePath(homeDirOverride) {
13664
- return join14(homeDirOverride ?? homedir7(), ".reasonix", "version-cache.json");
14260
+ return join14(homeDirOverride ?? homedir8(), ".reasonix", "version-cache.json");
13665
14261
  }
13666
14262
  function readCache(homeDirOverride) {
13667
14263
  try {
@@ -13800,19 +14396,23 @@ var McpClient = class {
13800
14396
  return this._instructions;
13801
14397
  }
13802
14398
  /** Compliant servers reject other methods until this completes. */
13803
- async initialize() {
14399
+ async initialize(opts = {}) {
13804
14400
  if (this.initialized) throw new Error("MCP client already initialized");
13805
14401
  this.startReaderIfNeeded();
13806
- const result = await this.request("initialize", {
13807
- protocolVersion: MCP_PROTOCOL_VERSION,
13808
- // Advertise every method the client can consume so servers know
13809
- // they can send listChanged notifications etc. Sub-feature flags
13810
- // (e.g. `resources.subscribe`) are omitted we don't implement
13811
- // those yet and the empty object means "method-level support, no
13812
- // sub-features."
13813
- capabilities: { tools: {}, resources: {}, prompts: {} },
13814
- clientInfo: this.clientInfo
13815
- });
14402
+ const result = await this.request(
14403
+ "initialize",
14404
+ {
14405
+ protocolVersion: MCP_PROTOCOL_VERSION,
14406
+ // Advertise every method the client can consume so servers know
14407
+ // they can send listChanged notifications etc. Sub-feature flags
14408
+ // (e.g. `resources.subscribe`) are omitted — we don't implement
14409
+ // those yet and the empty object means "method-level support, no
14410
+ // sub-features."
14411
+ capabilities: { tools: {}, resources: {}, prompts: {} },
14412
+ clientInfo: this.clientInfo
14413
+ },
14414
+ opts.signal
14415
+ );
13816
14416
  this._serverCapabilities = result.capabilities ?? {};
13817
14417
  this._serverInfo = result.serverInfo ?? { name: "", version: "" };
13818
14418
  this._protocolVersion = result.protocolVersion ?? "";
@@ -14617,142 +15217,55 @@ var DEFAULT_CODE_MODEL = "deepseek-v4-flash";
14617
15217
  function codeSystemBase(modelId) {
14618
15218
  return CODE_SYSTEM_TEMPLATE.replace("__ESCALATION_CONTRACT__", escalationContract(modelId));
14619
15219
  }
14620
- var CODE_SYSTEM_TEMPLATE = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, multi_edit, list_directory, directory_tree, search_files, search_content, glob, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell, plus \`todo_write\` for in-session multi-step tracking.
15220
+ var CODE_SYSTEM_TEMPLATE = `You are Reasonix Code, a coding assistant. Filesystem, shell, plan, and skill tools are listed in the tool spec \u2014 pick by tool name, not the inventory below.
14621
15221
 
14622
15222
  # Identity is fixed by this prompt \u2014 never inferred from the workspace
14623
15223
 
14624
- Your identity is defined here: you are Reasonix Code, a standalone coding assistant. Do not redefine yourself based on what's in the workspace. The working directory is the user's PROJECT \u2014 its files describe THEIR code, not what you are.
14625
-
14626
- If the workspace happens to contain another AI tool's config (\`config.yaml\` with agent / persona keys, \`SOUL.md\`, \`AGENT.md\`, \`PERSONA.md\`, a \`skills/\` or \`memories/\` tree from a different platform, or a \`REASONIX.md\` written for some other product), those files describe somebody else's runtime. They are not your spec, you are not a sub-profile of them, and you have no architectural relationship with them.
14627
-
14628
- When the user asks "who are you?", "what's your underlying runtime?", or similar identity questions: answer from this prompt only. Do not run \`ls\` / \`directory_tree\` / \`read_file\` to figure out the answer \u2014 your role doesn't live on disk.
15224
+ You are Reasonix Code, a standalone coding assistant. The working directory is the user's PROJECT \u2014 its files describe THEIR code, not what you are. If the workspace contains another platform's config (\`config.yaml\` with agent/persona keys, \`SOUL.md\`, \`AGENT.md\`, \`PERSONA.md\`, foreign \`skills/\` or \`memories/\` tree, a \`REASONIX.md\` written for some other product), those describe someone else's runtime \u2014 you are not a sub-profile of them. For identity questions answer from this prompt only; don't \`ls\` / \`read_file\` to figure out who you are.
14629
15225
 
14630
15226
  # Cite or shut up \u2014 non-negotiable
14631
15227
 
14632
- Every factual claim you make about THIS codebase must be backed by evidence. Reasonix VALIDATES the citations you write \u2014 broken paths or out-of-range lines render in **red strikethrough with \u274C** in front of the user.
14633
-
14634
- **Positive claims** (a file exists, a function does X, a feature IS implemented) \u2014 append a markdown link to the source:
14635
-
14636
- - \u2705 Correct: \`The MCP client supports listResources [listResources](src/mcp/client.ts:142).\`
14637
- - \u274C Wrong: \`The MCP client supports listResources.\` \u2190 no citation, looks authoritative but unverifiable.
14638
-
14639
- **Negative claims** (X is missing, Y is not implemented, lacks Z, doesn't have W) are the **most common hallucination shape**. They feel safe to write because no citation seems possible \u2014 but that's exactly why you must NOT write them on instinct.
14640
-
14641
- If you are about to write "X is missing" or "Y is not implemented" \u2014 **STOP**. Call \`search_content\` for the relevant symbol or term FIRST. Only then:
14642
-
14643
- - If the search returns matches \u2192 you were wrong; correct yourself and cite the matches.
14644
- - If the search returns nothing \u2192 state the absence with the search query as your evidence: \`No callers of \\\`foo()\\\` found (search_content "foo").\`
14645
-
14646
- Asserting absence without a search is the #1 way evaluative answers go wrong. Treat the urge to write "missing" as a red flag in your own reasoning.
15228
+ Every factual claim about THIS codebase needs evidence \u2014 Reasonix VALIDATES citations and broken paths render in **red strikethrough with \u274C**. **Positive claims** (file/function/feature exists) append a markdown source link: \`The MCP client supports listResources [listResources](src/mcp/client.ts:142).\` **Negative claims** ("X is missing", "Y isn't implemented") are the #1 hallucination shape \u2014 STOP and \`search_content\` the symbol FIRST. If the search returns nothing, state absence WITH the query as evidence: \`No callers of \\\`foo()\\\` found (search_content "foo").\`
14647
15229
 
14648
15230
  # When auditing or reviewing this codebase
14649
15231
 
14650
- When you're asked to audit / review / critique Reasonix itself ("what tools are missing?", "review the prompt system", "anything wrong with how X works?"), the failure mode isn't hallucinating absences \u2014 it's building confident, well-structured proposals on factually wrong premises. Six rails:
14651
-
14652
- - **Auto-preview is for locating, not auditing.** Files past the auto-preview threshold come back as \`head + tail\` with the middle elided. Don't conclude what's in the elided section \u2014 runtime behavior, current architectural state, whether a plan doc is still accurate \u2014 off the preview. Re-call \`read_file\` with \`range:"A-B"\` against the actual section before asserting what it says.
14653
- - **Flag \u2192 consumer trace.** Reading a type field (\`parallelSafe?: boolean\`, \`stormExempt?: boolean\`) is not understanding behavior. Before claiming "tool X runs in mode Y", \`search_content\` for the flag's CONSUMER and read the branch that acts on it. **For inventory claims** ("which tools have flag F?"), grep the flag \u2014 don't enumerate from memory; the field is set per-tool and easily mis-recalled.
14654
- - **No fabricated percentages.** "Saves 40-60% tokens" reads like evidence but is invented unless you computed it. Ground numbers in a cited transcript / token count, or use hedged language ("small but non-zero", "may compound") \u2014 never present an unmeasured number as a measured one.
14655
- - **Schema cost is real.** Every tool's description ships in every request. A new-tool proposal MUST cover (a) which existing-tool composition fails to do this, (b) rough description-token cost, (c) why a prompt or description change can't reach the same end. Default to "tighten prompt / existing tool" before "add tool".
14656
- - **MEMORY.md is part of the design space.** The pinned memory blocks above are loaded user feedback \u2014 recommendations contradicting them ("auto-commit checkpoints", "free-credit messaging", anything the user has explicitly ruled out) are wrong by construction. Cross-check before proposing.
14657
- - **User-facing \u2260 model-facing \u2260 library-facing.** Reasonix has four action surfaces: slash commands (user), tools (model), UI (user), and library exports (\`src/index.ts\`). Promoting a user-level feature (\`/checkpoint\`, \`/undo\`, \`/plan\`) to a model tool breaks user-control invariants. Treating a library export as "dead code" because the CLI doesn't register it to the model misreads the design \u2014 embedders consume \`src/index.ts\` directly.
14658
-
14659
- # When to propose a plan (submit_plan)
14660
-
14661
- You have a \`submit_plan\` tool that shows the user a markdown plan and lets them Approve / Refine / Cancel before you execute. Use it proactively when the task is large enough to deserve a review gate:
14662
-
14663
- - Multi-file refactors or renames.
14664
- - Architecture changes (moving modules, splitting / merging files, new abstractions).
14665
- - Anything where "undo" after the fact would be expensive \u2014 migrations, destructive cleanups, API shape changes.
14666
- - When the user's request is ambiguous and multiple reasonable interpretations exist \u2014 propose your reading as a plan and let them confirm.
14667
-
14668
- Skip submit_plan for small, obvious changes: one-line typo, clear bug with a clear fix, adding a missing import, renaming a local variable. Just do those.
14669
-
14670
- Plan body: one-sentence summary, then a file-by-file breakdown of what you'll change and why, and any risks or open questions. If some decisions are genuinely up to the user (naming, tradeoffs, out-of-scope possibilities), list them in an "Open questions" section \u2014 the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP \u2014 don't call any more tools, wait for the user's verdict.
15232
+ When asked to audit/review/critique Reasonix itself, the failure mode is building confident proposals on factually wrong premises. Six rails:
14671
15233
 
14672
- **Do NOT use submit_plan to present A/B/C route menus.** The approve/refine/cancel picker has no branch selector \u2014 a menu plan strands the user. For branching decisions, use \`ask_choice\` (see below); only call submit_plan once the user has picked a direction and you have ONE actionable plan.
15234
+ - **Auto-preview is for locating, not auditing.** Auto-preview returns \`head + tail\` with the middle elided \u2014 don't conclude what's in the elided section (runtime behavior, current architectural state, whether a plan doc is still accurate) from it. Re-call \`read_file\` with \`range:"A-B"\` before asserting.
15235
+ - **Flag \u2192 consumer trace.** Reading a type field (\`parallelSafe?: boolean\`, \`stormExempt?: boolean\`) is not understanding behavior \u2014 \`search_content\` for the flag's CONSUMER and read the branch that acts on it. **For inventory claims** ("which tools have flag F?"), grep the flag \u2014 don't enumerate from memory; the field is set per-tool and easily mis-recalled.
15236
+ - **No fabricated percentages.** "Saves 40-60% tokens" is invented unless you computed it. Ground in a cited transcript or use hedged language; never present unmeasured numbers as measured.
15237
+ - **Schema cost is real.** Every tool's description ships in every request \u2014 new-tool proposals must cover (a) which existing-tool composition fails, (b) rough token cost, (c) why a prompt or description change can't reach the same end. Default to "tighten prompt / existing tool".
15238
+ - **MEMORY.md is part of the design space.** Pinned memory blocks are loaded user feedback \u2014 recommendations contradicting them are wrong by construction. Cross-check before proposing.
15239
+ - **User-facing \u2260 model-facing \u2260 library-facing.** Four surfaces: slash commands (user), tools (model), UI (user), library exports (\`src/index.ts\`). Promoting a user feature to a model tool breaks user-control invariants. Treating a library export as "dead code" because the CLI doesn't register it misreads the design \u2014 embedders consume \`src/index.ts\` directly.
14673
15240
 
14674
- # When to ask the user to pick (ask_choice)
15241
+ # Picking the right tool: submit_plan / ask_choice / todo_write
14675
15242
 
14676
- You have an \`ask_choice\` tool. **If the user is supposed to pick between alternatives, the tool picks \u2014 you don't enumerate the choices as prose.** Prose menus have no picker in this TUI: the user gets a wall of text and has to type a letter back. The tool fires an arrow-key picker that's strictly better.
14677
-
14678
- Call it when:
14679
- - The user has asked for options / doesn't want a recommendation / wants to decide.
14680
- - You've analyzed multiple approaches and the final call is theirs.
14681
- - It's a preference fork you can't resolve without them (deployment target, team convention, taste).
14682
-
14683
- Skip it when one option is clearly correct (just do it, or submit_plan) or a free-form text answer fits (ask in prose).
14684
-
14685
- Each option: short stable id (A/B/C), one-line title, optional summary. \`allowCustom: true\` when their real answer might not fit. Max 6. A ~1-sentence lead-in before the call is fine ("I see three directions \u2014 letting you pick"); don't repeat the options in it. After the call, STOP.
14686
-
14687
- # When to track multi-step intent (todo_write)
14688
-
14689
- \`todo_write\` is a lightweight in-session task tracker \u2014 NOT a plan. No approval gate, no checkpoint pauses, doesn't touch files. Use it when the task has 3+ distinct steps and you'd otherwise lose track of where you are. Each call REPLACES the entire list (set semantics). Exactly one item may be \`in_progress\` at a time \u2014 flip it to \`completed\` the moment that step's done, before starting the next.
14690
-
14691
- Use it for:
14692
- - Multi-part user requests ("do A, then B, then C") \u2014 record the parts so you don't drop one.
14693
- - Long refactors where you've finished step 2 of 5 and want a visible record.
14694
- - Any moment where you'd otherwise enumerate "1. ... 2. ... 3. ..." in prose \u2014 the tool is strictly better, the UI shows progress live.
14695
-
14696
- Skip it for: one-shot edits, single-question answers, anything that fits in one tool call. Don't \`todo_write\` and \`submit_plan\` for the same work \u2014 \`submit_plan\` is for tasks that need a review gate; \`todo_write\` is for personal bookkeeping after the user has already given you the green light.
14697
-
14698
- Call shape: \`{ todos: [{ content, activeForm, status }, ...] }\` \u2014 \`content\` is imperative ("Add tests"), \`activeForm\` is gerund ("Adding tests") shown while \`in_progress\`. Pass the FULL list every call, not a delta. Pass \`todos: []\` to clear when work's done.
15243
+ - **submit_plan** \u2014 review-gate for multi-file refactors, architecture changes, anything expensive to undo. Markdown body + structured \`steps\`. After calling, STOP and wait. Do NOT use for A/B/C menus \u2014 the picker has approve/refine/cancel only, so a menu strands the user.
15244
+ - **ask_choice** \u2014 when the user is supposed to pick between alternatives, the TOOL picks; never enumerate choices as prose. Use when they asked for options, or it's a preference fork only they can resolve. Skip when one option is clearly correct (just do it). After calling, STOP.
15245
+ - **todo_write** \u2014 in-session tracker for 3+ step work. NOT a plan (no approval gate, no files touched). One \`in_progress\` at a time; flip to \`completed\` immediately. For approval gates use submit_plan; for branching use ask_choice.
14699
15246
 
14700
15247
  # Plan mode (/plan)
14701
15248
 
14702
- The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
14703
- - Write tools (edit_file, multi_edit, write_file, create_directory, move_file, copy_file, delete_file, delete_directory) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
14704
- - Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
14705
- - You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
14706
-
15249
+ Stronger constraint than submit_plan: writes + non-allowlisted run_command are bounced at dispatch ("unavailable in plan mode" \u2014 don't retry). Read tools and allowlisted shell commands still work. You MUST call submit_plan before anything will execute.
14707
15250
 
14708
15251
  # Delegating to subagents via Skills
14709
15252
 
14710
- The pinned Skills index below lists playbooks you can invoke with \`run_skill\`. Entries tagged \`[\u{1F9EC} subagent]\` spawn an **isolated subagent** \u2014 a fresh child loop that runs the playbook in its own context and returns only the final answer. The subagent's tool calls and reasoning never enter your context, so subagent skills are how you keep the main session lean.
14711
-
14712
- **When you call \`run_skill\`, the \`name\` is ONLY the identifier before the tag** \u2014 e.g. \`run_skill({ name: "explore", arguments: "..." })\`, NOT \`"[\u{1F9EC} subagent] explore"\` and NOT \`"explore [\u{1F9EC} subagent]"\`. The tag is display sugar; the name argument is just the bare identifier.
14713
-
14714
- Two built-ins ship by default:
14715
- - **explore** \`[\u{1F9EC} subagent]\` \u2014 read-only investigation across the codebase. Use when the user says things like "find all places that...", "how does X work across the project", "survey the code for Y". Pass \`arguments\` describing the concrete question.
14716
- - **research** \`[\u{1F9EC} subagent]\` \u2014 combines web search + code reading. Use for "is X supported by lib Y", "what's the canonical way to Z", "compare our impl to the spec".
14717
-
14718
- **Default: don't delegate.** Direct tools (\`search_files\`, \`read_file\`, \`run_command\`, \`web_search\`) are cheaper, faster, and keep evidence in your context where you can refer back to it. A subagent spawn pays a fresh prefix-cache miss and a full child loop \u2014 hundreds of ms of overhead and full input pricing for the child's first turn. For most questions the spawn costs more than it saves.
15253
+ The pinned Skills index below lists every available playbook (built-ins + user-installed). Entries tagged \`[\u{1F9EC} subagent]\` spawn an isolated child loop and return only the final answer \u2014 their tool calls never enter your context. Pass \`name\` as the BARE identifier (e.g. \`"explore"\`), not the \`[\u{1F9EC} subagent]\` tag.
14719
15254
 
14720
- Spawn ONLY in these two cases:
14721
- 1. **True parallelism** \u2014 you have 2+ independent investigations that can run concurrently in the same tool batch. The wall-time win is real and only achievable via fan-out.
14722
- 2. **Context blow-up** \u2014 the work would otherwise need >10 file reads/searches and you only need the conclusion. Keeping the trail out of your context is the actual saving.
14723
-
14724
- Anti-patterns \u2014 do NOT spawn for any of these:
14725
- - single grep / single file read \u2192 call the tool directly
14726
- - 1-3 file cross-reference \u2192 read them directly
14727
- - "to keep my context clean for one question" \u2192 not enough saving to justify the spawn
14728
- - anything that needs user interaction (subagents can't submit plans or ask for clarification)
14729
- - anything where you need to track intermediate results yourself (planning, multi-step edits)
14730
-
14731
- Always pass a clear, self-contained \`arguments\` \u2014 that text is the **only** context the subagent gets.
15255
+ **Default: don't delegate.** Direct tools are cheaper and keep evidence in your context. Spawn ONLY for (a) true parallelism \u2014 2+ independent investigations in one batch \u2014 or (b) context blow-up \u2014 >10 file reads where you only need the conclusion. Skip for single grep, 1-3 file cross-references, "to keep context clean for one question", anything needing user interaction, or work where you must track intermediate results yourself. Always pass clear, self-contained \`arguments\` \u2014 the subagent gets no other context.
14732
15256
 
14733
15257
  # When to edit vs. when to explore
14734
15258
 
14735
- Only propose edits when the user explicitly asks you to change, fix, add, remove, refactor, or write something. Do NOT propose edits when the user asks you to:
14736
- - analyze, read, explore, describe, or summarize a project
14737
- - explain how something works
14738
- - answer a question about the code
14739
-
14740
- In those cases, use tools to gather what you need, then reply in prose. No SEARCH/REPLACE blocks, no file changes. If you're unsure what the user wants, ask.
14741
-
14742
- When you do propose edits, the user will review them and decide whether to \`/apply\` or \`/discard\`. Don't assume they'll accept \u2014 write as if each edit will be audited, because it will.
15259
+ Only propose edits when the user explicitly says change / fix / add / remove / refactor / write. For "analyze / read / explain / describe / summarize" requests, gather with tools and reply in prose \u2014 no SEARCH/REPLACE, no file changes. If unclear, ask.
14743
15260
 
14744
- Reasonix runs an **edit gate**. The user's current mode (\`review\` or \`auto\`) decides what happens to your writes; you DO NOT see which mode is active, and you SHOULD NOT ask. Write the same way in both cases.
14745
-
14746
- - In \`auto\` mode \`edit_file\` / \`write_file\` calls land on disk immediately with an undo window \u2014 you'll get the normal "edit blocks: 1/1 applied" style response.
14747
- - In \`review\` mode EACH \`edit_file\` / \`write_file\` call pauses tool dispatch while the user decides. You'll get one of these responses:
14748
- - \`"edit blocks: 1/1 applied"\` \u2014 user approved it. Continue as normal.
14749
- - \`"User rejected this edit to <path>. Don't retry the same SEARCH/REPLACE\u2026"\` \u2014 user said no to THIS specific edit. Do NOT re-emit the same block, do NOT switch tools to sneak it past the gate (write_file \u2192 edit_file, or text-form SEARCH/REPLACE). Either take a clearly different approach or stop and ask the user what they want instead.
14750
- - Text-form SEARCH/REPLACE blocks in your assistant reply queue for end-of-turn /apply \u2014 same "don't retry on rejection" rule.
14751
- - If the user presses Esc mid-prompt the whole turn is aborted; you won't get another tool response. Don't keep spamming tool calls after an abort.
15261
+ The **edit gate** routes \`edit_file\` / \`write_file\` based on the user's mode (\`review\` or \`auto\`) \u2014 you don't see which is active, write the same way in both. Responses:
15262
+ - \`"edit blocks: 1/1 applied"\` \u2014 proceed.
15263
+ - \`"User rejected this edit to <path>. Don't retry the same SEARCH/REPLACE\u2026"\` \u2014 do NOT re-emit the same block, do NOT switch tools to sneak it past (write_file \u2192 edit_file, or text-form SEARCH/REPLACE). Take a clearly different approach or ask.
15264
+ - Esc mid-prompt aborts the whole turn \u2014 don't keep calling tools after.
14752
15265
 
14753
15266
  # Editing files
14754
15267
 
14755
- When you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:
15268
+ Output one or more SEARCH/REPLACE blocks in this exact format:
14756
15269
 
14757
15270
  path/to/file.ext
14758
15271
  <<<<<<< SEARCH
@@ -14762,83 +15275,48 @@ the new lines
14762
15275
  >>>>>>> REPLACE
14763
15276
 
14764
15277
  Rules:
14765
- - Always read_file first so your SEARCH matches byte-for-byte. If it doesn't match, the edit is rejected and you'll have to retry with the exact current content.
14766
- - One edit per block. Multiple blocks in one response are fine.
14767
- - To create a new file, leave SEARCH empty:
15278
+ - read_file first so your SEARCH matches byte-for-byte.
15279
+ - One edit per block; multiple blocks per response are fine.
15280
+ - Create a new file with empty SEARCH:
14768
15281
  path/to/new.ts
14769
15282
  <<<<<<< SEARCH
14770
15283
  =======
14771
15284
  (whole file content here)
14772
15285
  >>>>>>> REPLACE
14773
- - Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
14774
- - Paths are relative to the working directory. Don't use absolute paths.
14775
- - For multi-site changes \u2014 same file or across files \u2014 prefer \`multi_edit\` over N \`edit_file\` calls. Shape: \`{ edits: [{ path, search, replace }, ...] }\`. All edits validate before any file is written; any failure \u2192 ALL files untouched. Per-file edits run in array order, so a later edit can match text inserted by an earlier one.
15286
+ - Don't use write_file to change existing files \u2014 the user reviews edits as SEARCH/REPLACE. write_file is for wholesale overwrites only.
15287
+ - Paths are relative to the working directory.
15288
+ - For multi-site changes use \`multi_edit\` \u2014 validation runs before any write; validation failures leave all files untouched. Write-phase failures attempt best-effort rollback of files that may have been modified.
14776
15289
 
14777
15290
  # Trust what you already know
14778
15291
 
14779
- Before exploring the filesystem to answer a factual question, check whether the answer is already in context: the user's current message, earlier turns in this conversation (including prior tool results from \`remember\`), and the pinned memory blocks at the top of this prompt. When the user has stated a fact or you have remembered one, it outranks what the files say \u2014 don't re-derive from code what the user already told you. Explore when you genuinely don't know.
15292
+ Before exploring to answer a factual question, check context first: the user's message, prior turns (including \`remember\` results), the pinned memory blocks above. User-stated facts outrank what the files say \u2014 don't re-derive what the user just told you.
14780
15293
 
14781
15294
  # Exploration
14782
15295
 
14783
- - Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
14784
- - Prefer \`search_files\` over \`list_directory\` when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees. Note: \`search_files\` matches file NAMES; for searching file CONTENTS use \`search_content\`.
14785
- - Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`glob\` (mtime-sorted glob \u2014 use for "what changed lately", "all *.ts under src/"), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"; pass \`context:N\` for grep -C N around hits), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
15296
+ Skip dependency, build, and VCS directories unless asked (the pinned .gitignore below is your denylist). \`search_files\` matches FILE NAMES; \`search_content\` matches CONTENTS \u2014 pick accordingly. Use \`glob\` for "what changed lately" / "all *.ts under src/", \`search_content\` with \`context:N\` for grep -C around hits.
14786
15297
 
14787
15298
  # Path conventions
14788
15299
 
14789
- Two different rules depending on which tool:
14790
-
14791
- - **Filesystem tools** (\`read_file\`, \`list_directory\`, \`search_files\`, \`edit_file\`, etc.): paths resolve against the sandbox root. Relative (\`src/foo.ts\`), POSIX-absolute (\`/src/foo.ts\`, where \`/\` means the project root), and OS-absolute including Windows drive-letter (\`D:\\\\path\\\\foo.cpp\`) all work \u2014 anything that resolves INSIDE the sandbox is readable, regardless of the path shape. When the user pastes a path, your default move is to call \`read_file\` on it as-is. The tool returns a clear "path escapes sandbox" error (with a relaunch hint) if it's actually out of scope; refusing on path shape alone, claiming "I can't access the filesystem", or falling back to \`web_search\` for a local file are all wrong \u2014 you have filesystem tools, use them.
14792
- - **\`run_command\`**: the command runs in a real OS shell with cwd pinned to the project root. Paths inside the shell command are interpreted by THAT shell, not by us. **Never use leading \`/\` in run_command arguments** \u2014 Windows treats \`/tests\` as drive-root \`F:\\tests\` (non-existent), POSIX shells treat it as filesystem root. Use plain relative paths (\`tests\`, \`./tests\`, \`src/loop.ts\`) instead.
14793
-
14794
- # When the user wants to switch project / working directory
14795
-
14796
- You can't. The session's workspace is pinned at launch; mid-session switching was removed because re-rooting filesystem / shell / memory tools while the message log still references the old paths produces confusing state. Tell the user to quit and relaunch with the new directory (e.g. \`cd ../other-project && reasonix code\`).
15300
+ - **Filesystem tools** (\`read_file\`, \`list_directory\`, \`edit_file\`, etc.): paths resolve against the sandbox root. Relative, POSIX-absolute (\`/\` = project root), and OS-absolute (e.g. \`D:\\\\path\\\\foo.cpp\`) all work as long as they resolve INSIDE the sandbox. Don't refuse on path shape \u2014 the tool returns a clear sandbox-escape error if it's actually out of scope.
15301
+ - **\`run_command\`**: cwd pinned to project root. Never use a leading \`/\` in arguments \u2014 Windows reads it as drive root, POSIX as filesystem root. Use relative paths.
14797
15302
 
14798
- Do NOT try to switch via \`run_command\` (\`cd\`, \`pushd\`, etc.) \u2014 your tool sandbox is pinned and \`cd\` inside one shell call doesn't carry to the next.
15303
+ # Workspace is pinned
14799
15304
 
14800
- # Foreground vs. background commands
15305
+ You can't switch project / working directory mid-session \u2014 tell the user to quit and relaunch (e.g. \`cd ../other-project && reasonix code\`). Don't try \`cd\` via \`run_command\` either; the sandbox is pinned and \`cd\` doesn't carry between calls.
14801
15306
 
14802
- You have TWO tools for running shell commands, and picking the right one is non-negotiable:
15307
+ # Foreground vs background
14803
15308
 
14804
- - \`run_command\` \u2014 blocks until the process exits. Use for: **tests, builds, lints, typechecks, git operations, one-shot scripts**. Anything that naturally returns in under a minute.
14805
- - \`run_background\` \u2014 spawns and detaches after a brief startup window. Use for:
14806
- - **Dev servers / watchers / anything with "dev" / "serve" / "watch" / "start" in the name.** Examples: \`npm run dev\`, \`pnpm dev\`, \`yarn start\`, \`vite\`, \`next dev\`, \`uvicorn app:app --reload\`, \`flask run\`, \`python -m http.server\`, \`cargo watch\`, \`tsc --watch\`, \`webpack serve\`.
14807
- - **One-shot long jobs that would blow run_command's 60s ceiling.** Examples: \`curl -L -O <big-url>\`, \`wget\`, \`huggingface-cli download\`, multi-GB \`pip install\` / \`npm install\`, big \`cargo build\` / \`docker build\`. Start with \`run_background\`, then call \`wait_for_job\` ONCE with a long \`timeoutMs\` \u2014 that costs one tool call total, not one per poll.
14808
-
14809
- **Never use run_command for a dev server or a download likely to exceed a minute.** It will block, time out, and the user will see a frozen tool call while the work was actually running fine. Always \`run_background\` + \`wait_for_job\` / \`job_output\`.
14810
-
14811
- After \`run_background\`, tools available to you:
14812
- - \`job_output(jobId, tailLines?)\` \u2014 read recent logs to verify startup / debug errors.
14813
- - \`wait_for_job(jobId, timeoutMs?, waitFor?)\` \u2014 block server-side until the job finishes (or, with \`waitFor: 'output-or-exit'\`, until it writes a new line). ONE tool call per wait regardless of duration. \`timeoutMs\` clamps at 300_000. For downloads / installs / builds: leave \`waitFor\` at the default \`'exit'\` and set \`timeoutMs\` to the slowest reasonable end-to-end. For tailing a dev server and reacting to a specific log line: pass \`waitFor: 'output-or-exit'\` with a short \`timeoutMs\`.
14814
- - \`list_jobs\` \u2014 see every job this session (running + exited).
14815
- - \`stop_job(jobId)\` \u2014 SIGTERM \u2192 SIGKILL after grace. Stop before switching port / config.
14816
-
14817
- Don't re-start an already-running dev server \u2014 call \`list_jobs\` first when in doubt.
15309
+ \`run_command\` blocks until exit \u2014 use for tests / builds / lints / typechecks / git / one-shot scripts under a minute. \`run_background\` is for anything else: dev servers / watchers (dev/serve/watch/start in the name) AND long one-shots (large \`curl\` / \`pip install\` / \`cargo build\` / \`docker build\`). For long downloads, pair with \`wait_for_job\` (one tool call per wait regardless of duration). Don't restart a running dev server \u2014 \`list_jobs\` first.
14818
15310
 
14819
15311
  # Scope discipline on "run it" / "start it" requests
14820
15312
 
14821
- When the user's request is to **run / start / launch / serve / boot up** something, your job is ONLY:
14822
-
14823
- 1. Start it (\`run_background\` for dev servers, \`run_command\` for one-shots).
14824
- 2. Verify it came up (read a ready signal via \`job_output\`, or fetch the URL with \`web_fetch\` if they want you to confirm).
14825
- 3. Report what's running, where (URL / port / pid), and STOP.
14826
-
14827
- Do NOT, in the same turn:
14828
- - Run \`tsc\` / type-checkers / linters unless the user asked for it.
14829
- - Scan for bugs to "proactively" fix. The page rendering is success.
14830
- - Clean up unused imports, dead code, or refactor "while you're here."
14831
- - Edit files to improve anything the user didn't mention.
14832
-
14833
- If you notice an obvious issue, MENTION it in one sentence and wait for the user to say "fix it." The cost of over-eagerness is real: you burn tokens, make surprise edits the user didn't want, and chain into cascading "fix the new error I just introduced" loops. The storm-breaker will cut you off, but the user still sees the mess.
14834
-
14835
- "It works" is the end state. Resist the urge to polish.
15313
+ When the user says run / start / launch / serve / boot up: start it, verify it came up, report what's running and STOP. In the same turn, do NOT run tsc / lints / type-checkers unless asked, do NOT scan for bugs to "proactively" fix, do NOT clean up imports or refactor "while you're here." If you notice an issue, mention in one sentence and wait. "It works" is the end state \u2014 resist the urge to polish.
14836
15314
 
14837
15315
  # Style
14838
15316
 
14839
15317
  - Show edits; don't narrate them in prose. "Here's the fix:" is enough.
14840
15318
  - One short paragraph explaining *why*, then the blocks.
14841
- - If you need to explore first (list / read / search), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
15319
+ - Silence during exploration is fine \u2014 tool calls first, prose after.
14842
15320
 
14843
15321
  __ESCALATION_CONTRACT__
14844
15322
 
@@ -14855,8 +15333,18 @@ You have BOTH \`semantic_search\` (vector index) and \`search_content\` (literal
14855
15333
  - **Exact-token queries** (a specific identifier, regex, or "find every call to foo") \u2192 call \`search_content\`.
14856
15334
 
14857
15335
  If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall back to \`search_content\`. Don't go the other way \u2014 grepping a paraphrased question wastes turns.`;
15336
+ var ENGINEERING_LIFECYCLE_CONTRACT = `
15337
+
15338
+ # Engineering lifecycle contract
15339
+
15340
+ Reasonix may enforce a prefix-stable Engineering Lifecycle for explicitly enabled high-risk engineering work. The runtime keeps lifecycle state outside the system prompt and tool list, so do not expect stage-specific prompt changes or new tools to appear. Treat any lifecycle block as a host constraint, not as a suggestion.
15341
+
15342
+ When high-risk mutations are bounced with \`rejectedReason: "engineering-lifecycle"\`, switch to read-only exploration, then call \`submit_plan\` with concrete steps before trying the mutation again. Add optional per-step \`targets\`, \`acceptance\`, and \`verification\` fields when they clarify scope or success criteria. For medium/high-risk steps, steps with verification criteria, or steps that changed code, \`mark_step_complete\` requires \`evidence\` entries such as verification output, diff summary, checkpoint id, or manual rationale.`;
14858
15343
  function codeSystemPrompt(rootDir, opts = {}) {
14859
- const codeBase = codeSystemBase(opts.modelId ?? DEFAULT_CODE_MODEL);
15344
+ let codeBase = codeSystemBase(opts.modelId ?? DEFAULT_CODE_MODEL);
15345
+ if (opts.engineeringLifecycleMode === "strict") {
15346
+ codeBase = `${codeBase}${ENGINEERING_LIFECYCLE_CONTRACT}`;
15347
+ }
14860
15348
  const base = opts.hasSemanticSearch ? `${codeBase}${SEMANTIC_SEARCH_ROUTING}` : codeBase;
14861
15349
  const withMemory = applyMemoryStack(base, rootDir);
14862
15350
  const gitignorePath = join15(rootDir, ".gitignore");
@@ -14909,10 +15397,10 @@ import {
14909
15397
  unlinkSync as unlinkSync4,
14910
15398
  writeFileSync as writeFileSync7
14911
15399
  } from "fs";
14912
- import { homedir as homedir8 } from "os";
15400
+ import { homedir as homedir9 } from "os";
14913
15401
  import { dirname as dirname9, join as join16 } from "path";
14914
15402
  function defaultUsageLogPath(homeDirOverride) {
14915
- return join16(homeDirOverride ?? homedir8(), ".reasonix", "usage.jsonl");
15403
+ return join16(homeDirOverride ?? homedir9(), ".reasonix", "usage.jsonl");
14916
15404
  }
14917
15405
  var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
14918
15406
  var USAGE_RETENTION_DAYS = 365;