reasonix 0.40.0 → 0.41.0

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 (130) hide show
  1. package/README.md +21 -13
  2. package/README.zh-CN.md +19 -13
  3. package/dashboard/app.css +8 -4
  4. package/dashboard/dist/app.js +279 -224
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/dist/cli/acp-64VQZLDJ.js +708 -0
  7. package/dist/cli/acp-64VQZLDJ.js.map +1 -0
  8. package/dist/cli/chat-ZAGX52RV.js +46 -0
  9. package/dist/cli/{chunk-UCMTWZKU.js → chunk-2CXPDAWX.js} +2 -2
  10. package/dist/cli/{chunk-CLAN6PVH.js → chunk-4H3ZRJ2U.js} +19 -7
  11. package/dist/cli/chunk-4H3ZRJ2U.js.map +1 -0
  12. package/dist/cli/{chunk-A5LSGEEK.js → chunk-4W2CICFQ.js} +21 -10
  13. package/dist/cli/{chunk-A5LSGEEK.js.map → chunk-4W2CICFQ.js.map} +1 -1
  14. package/dist/cli/{chunk-CZSJILQP.js → chunk-65Q5HQ26.js} +39 -1
  15. package/dist/cli/chunk-65Q5HQ26.js.map +1 -0
  16. package/dist/cli/{chunk-XHQIK7B6.js → chunk-7SPOFTMT.js} +2 -2
  17. package/dist/cli/{chunk-5GKJLNP2.js → chunk-7VFNPMKG.js} +2 -2
  18. package/dist/cli/{chunk-UVRXTSK3.js → chunk-A3LL4XDV.js} +8 -2
  19. package/dist/cli/chunk-A3LL4XDV.js.map +1 -0
  20. package/dist/cli/{chunk-VLNRQMCI.js → chunk-A7VHMMDE.js} +2 -2
  21. package/dist/cli/{chunk-R4YTW7PR.js → chunk-ARF3N2SY.js} +56 -12
  22. package/dist/cli/chunk-ARF3N2SY.js.map +1 -0
  23. package/dist/cli/{chunk-AVB3WZWU.js → chunk-AT6GGIBV.js} +10 -10
  24. package/dist/cli/{chunk-RFX7TYVV.js → chunk-BOFL3T45.js} +14 -1
  25. package/dist/cli/chunk-BOFL3T45.js.map +1 -0
  26. package/dist/cli/{chunk-SZH34P45.js → chunk-BYZGO3BX.js} +43 -17
  27. package/dist/cli/chunk-BYZGO3BX.js.map +1 -0
  28. package/dist/cli/{chunk-7DLHHBGN.js → chunk-CD4SCQL4.js} +6 -4
  29. package/dist/cli/chunk-CD4SCQL4.js.map +1 -0
  30. package/dist/cli/{chunk-HCC42PEI.js → chunk-CFY2XLY6.js} +6 -2
  31. package/dist/cli/chunk-CFY2XLY6.js.map +1 -0
  32. package/dist/cli/{chunk-26UDIXLD.js → chunk-F2AV2QDK.js} +493 -460
  33. package/dist/cli/chunk-F2AV2QDK.js.map +1 -0
  34. package/dist/cli/{chunk-KMWKGPFZ.js → chunk-H4OLWRSX.js} +10 -1
  35. package/dist/cli/chunk-H4OLWRSX.js.map +1 -0
  36. package/dist/cli/{chunk-4YV2GBYG.js → chunk-IEA6JOIP.js} +291 -98
  37. package/dist/cli/chunk-IEA6JOIP.js.map +1 -0
  38. package/dist/cli/{chunk-WKOMCPXP.js → chunk-KZYLMMU5.js} +21 -13
  39. package/dist/cli/chunk-KZYLMMU5.js.map +1 -0
  40. package/dist/cli/{chunk-JWCTX5S4.js → chunk-L7W3HJZQ.js} +2 -2
  41. package/dist/cli/{chunk-MRLXEMZ7.js → chunk-LN27AKV3.js} +1 -1
  42. package/dist/cli/chunk-LN27AKV3.js.map +1 -0
  43. package/dist/cli/{chunk-IYF36OCJ.js → chunk-LTXADNCO.js} +2 -2
  44. package/dist/cli/{chunk-H7PHYVPM.js → chunk-MHGPBJ2T.js} +44 -8
  45. package/dist/cli/chunk-MHGPBJ2T.js.map +1 -0
  46. package/dist/cli/{chunk-ULBW7DYL.js → chunk-RAUPWSYA.js} +2 -2
  47. package/dist/cli/chunk-SXLJBFIV.js +245 -0
  48. package/dist/cli/chunk-SXLJBFIV.js.map +1 -0
  49. package/dist/cli/{chunk-4X3NY5ZM.js → chunk-UV7XJUJH.js} +2 -2
  50. package/dist/cli/{chunk-XJLZ4HKU.js → chunk-VFG4GIT3.js} +2 -2
  51. package/dist/cli/{chunk-FFNOMR32.js → chunk-WE3YZULK.js} +2 -2
  52. package/dist/cli/chunk-Y5XNV3NX.js +25 -0
  53. package/dist/cli/chunk-Y5XNV3NX.js.map +1 -0
  54. package/dist/cli/{chunk-XST7BSZJ.js → chunk-YJFKFTAL.js} +7 -1
  55. package/dist/cli/chunk-YJFKFTAL.js.map +1 -0
  56. package/dist/cli/{code-YQGVLIT2.js → code-X3M6ENTQ.js} +38 -35
  57. package/dist/cli/{code-YQGVLIT2.js.map → code-X3M6ENTQ.js.map} +1 -1
  58. package/dist/cli/{commands-FQZOBLLZ.js → commands-QY7MSQG7.js} +4 -4
  59. package/dist/cli/{commit-ZS24SHPG.js → commit-BRCQ3OQO.js} +3 -3
  60. package/dist/cli/{desktop-6OLENOOO.js → desktop-ZTMHQR2Y.js} +247 -28
  61. package/dist/cli/desktop-ZTMHQR2Y.js.map +1 -0
  62. package/dist/cli/{diff-2VUKNGEI.js → diff-YASCB7PU.js} +7 -7
  63. package/dist/cli/{doctor-JO2WNN6C.js → doctor-XCN5ETVP.js} +9 -9
  64. package/dist/cli/{events-APSVNROZ.js → events-2AJTXR7I.js} +3 -3
  65. package/dist/cli/index.js +69 -35
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/cli/{mcp-DCKOE5RF.js → mcp-YMWBLRR7.js} +2 -2
  68. package/dist/cli/{mcp-browse-D6GBP5RQ.js → mcp-browse-XLDUE6SB.js} +7 -3
  69. package/dist/cli/mcp-browse-XLDUE6SB.js.map +1 -0
  70. package/dist/cli/{mcp-inspect-KFGFPJ3E.js → mcp-inspect-H4D2HSJP.js} +5 -7
  71. package/dist/cli/{mcp-inspect-KFGFPJ3E.js.map → mcp-inspect-H4D2HSJP.js.map} +1 -1
  72. package/dist/cli/{prompt-PKCCLLAD.js → prompt-RSIHN62V.js} +4 -3
  73. package/dist/cli/{prune-sessions-LV33R47N.js → prune-sessions-4N3BVST2.js} +2 -2
  74. package/dist/cli/{replay-WFCYX7XF.js → replay-3GTWM75X.js} +8 -8
  75. package/dist/cli/{run-IUJYEPMT.js → run-BLZPTRDX.js} +19 -21
  76. package/dist/cli/{run-IUJYEPMT.js.map → run-BLZPTRDX.js.map} +1 -1
  77. package/dist/cli/{server-CN4QPPVJ.js → server-DRFPXXSH.js} +16 -12
  78. package/dist/cli/{server-CN4QPPVJ.js.map → server-DRFPXXSH.js.map} +1 -1
  79. package/dist/cli/{sessions-F5GPGTJN.js → sessions-BOWFPTXT.js} +13 -13
  80. package/dist/cli/{setup-WWMDBPSB.js → setup-FQL2JJC2.js} +5 -5
  81. package/dist/cli/version-XQXYSJ5L.js +30 -0
  82. package/dist/index.d.ts +148 -103
  83. package/dist/index.js +468 -134
  84. package/dist/index.js.map +1 -1
  85. package/package.json +2 -1
  86. package/dist/cli/chat-G7CUW4ZI.js +0 -45
  87. package/dist/cli/chunk-26UDIXLD.js.map +0 -1
  88. package/dist/cli/chunk-4YV2GBYG.js.map +0 -1
  89. package/dist/cli/chunk-7DLHHBGN.js.map +0 -1
  90. package/dist/cli/chunk-CLAN6PVH.js.map +0 -1
  91. package/dist/cli/chunk-CPTZ5OHX.js +0 -18
  92. package/dist/cli/chunk-CPTZ5OHX.js.map +0 -1
  93. package/dist/cli/chunk-CZSJILQP.js.map +0 -1
  94. package/dist/cli/chunk-H7PHYVPM.js.map +0 -1
  95. package/dist/cli/chunk-HCC42PEI.js.map +0 -1
  96. package/dist/cli/chunk-KMWKGPFZ.js.map +0 -1
  97. package/dist/cli/chunk-MRLXEMZ7.js.map +0 -1
  98. package/dist/cli/chunk-R4YTW7PR.js.map +0 -1
  99. package/dist/cli/chunk-RFX7TYVV.js.map +0 -1
  100. package/dist/cli/chunk-SZH34P45.js.map +0 -1
  101. package/dist/cli/chunk-UVRXTSK3.js.map +0 -1
  102. package/dist/cli/chunk-WKOMCPXP.js.map +0 -1
  103. package/dist/cli/chunk-XST7BSZJ.js.map +0 -1
  104. package/dist/cli/desktop-6OLENOOO.js.map +0 -1
  105. package/dist/cli/mcp-browse-D6GBP5RQ.js.map +0 -1
  106. package/dist/cli/version-KQUPV6T5.js +0 -30
  107. /package/dist/cli/{chat-G7CUW4ZI.js.map → chat-ZAGX52RV.js.map} +0 -0
  108. /package/dist/cli/{chunk-UCMTWZKU.js.map → chunk-2CXPDAWX.js.map} +0 -0
  109. /package/dist/cli/{chunk-XHQIK7B6.js.map → chunk-7SPOFTMT.js.map} +0 -0
  110. /package/dist/cli/{chunk-5GKJLNP2.js.map → chunk-7VFNPMKG.js.map} +0 -0
  111. /package/dist/cli/{chunk-VLNRQMCI.js.map → chunk-A7VHMMDE.js.map} +0 -0
  112. /package/dist/cli/{chunk-AVB3WZWU.js.map → chunk-AT6GGIBV.js.map} +0 -0
  113. /package/dist/cli/{chunk-JWCTX5S4.js.map → chunk-L7W3HJZQ.js.map} +0 -0
  114. /package/dist/cli/{chunk-IYF36OCJ.js.map → chunk-LTXADNCO.js.map} +0 -0
  115. /package/dist/cli/{chunk-ULBW7DYL.js.map → chunk-RAUPWSYA.js.map} +0 -0
  116. /package/dist/cli/{chunk-4X3NY5ZM.js.map → chunk-UV7XJUJH.js.map} +0 -0
  117. /package/dist/cli/{chunk-XJLZ4HKU.js.map → chunk-VFG4GIT3.js.map} +0 -0
  118. /package/dist/cli/{chunk-FFNOMR32.js.map → chunk-WE3YZULK.js.map} +0 -0
  119. /package/dist/cli/{commands-FQZOBLLZ.js.map → commands-QY7MSQG7.js.map} +0 -0
  120. /package/dist/cli/{commit-ZS24SHPG.js.map → commit-BRCQ3OQO.js.map} +0 -0
  121. /package/dist/cli/{diff-2VUKNGEI.js.map → diff-YASCB7PU.js.map} +0 -0
  122. /package/dist/cli/{doctor-JO2WNN6C.js.map → doctor-XCN5ETVP.js.map} +0 -0
  123. /package/dist/cli/{events-APSVNROZ.js.map → events-2AJTXR7I.js.map} +0 -0
  124. /package/dist/cli/{mcp-DCKOE5RF.js.map → mcp-YMWBLRR7.js.map} +0 -0
  125. /package/dist/cli/{prompt-PKCCLLAD.js.map → prompt-RSIHN62V.js.map} +0 -0
  126. /package/dist/cli/{prune-sessions-LV33R47N.js.map → prune-sessions-4N3BVST2.js.map} +0 -0
  127. /package/dist/cli/{replay-WFCYX7XF.js.map → replay-3GTWM75X.js.map} +0 -0
  128. /package/dist/cli/{sessions-F5GPGTJN.js.map → sessions-BOWFPTXT.js.map} +0 -0
  129. /package/dist/cli/{setup-WWMDBPSB.js.map → setup-FQL2JJC2.js.map} +0 -0
  130. /package/dist/cli/{version-KQUPV6T5.js.map → version-XQXYSJ5L.js.map} +0 -0
package/dist/index.js CHANGED
@@ -773,6 +773,42 @@ var DEFAULT_INDEX_EXCLUDES = {
773
773
  var DEFAULT_MAX_FILE_BYTES = 256 * 1024;
774
774
 
775
775
  // src/config.ts
776
+ var BUILTIN_TYPE_DOCS = {
777
+ user: "role / skills / preferences",
778
+ feedback: "corrections or confirmed approaches",
779
+ project: "facts / decisions about the current work",
780
+ reference: "pointers to external systems the user uses"
781
+ };
782
+ function loadMemoryTypeRegistry(cfg = readConfig()) {
783
+ const out = [];
784
+ for (const name of ["user", "feedback", "project", "reference"]) {
785
+ out.push({ name, builtin: true, description: BUILTIN_TYPE_DOCS[name] });
786
+ }
787
+ const seen = new Set(out.map((e) => e.name));
788
+ for (const raw of cfg.memory?.customTypes ?? []) {
789
+ if (!raw || typeof raw.name !== "string") continue;
790
+ const name = raw.name.trim();
791
+ if (!name || !/^[a-zA-Z][a-zA-Z0-9_-]{0,31}$/.test(name)) continue;
792
+ if (seen.has(name)) continue;
793
+ seen.add(name);
794
+ const entry = { name, builtin: false };
795
+ if (typeof raw.description === "string") entry.description = raw.description;
796
+ if (raw.priority === "low" || raw.priority === "medium" || raw.priority === "high") {
797
+ entry.priority = raw.priority;
798
+ }
799
+ if (raw.expires === "project_end") entry.expires = raw.expires;
800
+ out.push(entry);
801
+ }
802
+ return out;
803
+ }
804
+ function memoryTypeDefaults(typeName, cfg = readConfig()) {
805
+ const found = loadMemoryTypeRegistry(cfg).find((e) => e.name === typeName);
806
+ if (!found) return {};
807
+ const out = {};
808
+ if (found.priority) out.priority = found.priority;
809
+ if (found.expires) out.expires = found.expires;
810
+ return out;
811
+ }
776
812
  function defaultConfigPath() {
777
813
  return join(homedir(), ".reasonix", "config.json");
778
814
  }
@@ -891,7 +927,10 @@ var EN = {
891
927
  cancel: "Cancel",
892
928
  confirm: "Confirm",
893
929
  back: "Back",
894
- next: "Next"
930
+ next: "Next",
931
+ tool: "tool",
932
+ running: "running",
933
+ noTurns: "(no turns yet)"
895
934
  },
896
935
  cli: {
897
936
  description: "DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.",
@@ -1363,9 +1402,18 @@ var EN = {
1363
1402
  counterDone: "{done}/{total} done ({pct}%) \xB7 {total} steps",
1364
1403
  counterDoneSingular: "{done}/{total} done ({pct}%) \xB7 {total} step"
1365
1404
  },
1405
+ noPlanSummary: "No plan body submitted yet.",
1406
+ detailCollapsedHint: "Ctrl+P expands full plan details.",
1407
+ detailExpandedHint: "Ctrl+P collapses details.",
1408
+ detailHeader: "Plan details",
1409
+ detailWindow: "showing lines {start}-{end} of {total}",
1410
+ detailScrollHint: "PgUp/PgDn scroll details \xB7 Home/End jump",
1366
1411
  reviseTitle: "Revise plan",
1367
1412
  reviseSteps: "{count} steps",
1368
- reviseFooter: "\u2191\u2193 focus \xB7 space toggle skip \xB7 k/j move \xB7 \u23CE accept \xB7 esc cancel"
1413
+ reviseFooter: "\u2191\u2193 focus \xB7 space toggle skip \xB7 k/j move \xB7 \u23CE accept \xB7 esc cancel",
1414
+ riskMed: " med",
1415
+ riskHigh: " high",
1416
+ completeMsg: "\u25B8 plan complete \u2014 all {total} step{s} done \xB7 archived"
1369
1417
  },
1370
1418
  app: {
1371
1419
  walkCancelledRemaining: "\u25B8 walk cancelled \u2014 {count} block(s) still pending.",
@@ -1385,6 +1433,9 @@ var EN = {
1385
1433
  notedVerbAppended: "appended to",
1386
1434
  memoryWriteFailed: "# memory write failed",
1387
1435
  commandFailed: "! command failed",
1436
+ btwUsage: "\u25B8 /btw <question> \u2014 ask a side question without polluting the conversation context.",
1437
+ btwHeader: "\u226B btw",
1438
+ btwFailed: "/btw failed",
1388
1439
  restoreCodeOnly: "\u25B8 /restore is code-mode only",
1389
1440
  hookUserPromptSubmit: "UserPromptSubmit hook",
1390
1441
  hookStop: "Stop hook",
@@ -1427,6 +1478,7 @@ var EN = {
1427
1478
  flashEscalation: "\u21E7 flash requested escalation \u2014 retrying this turn on {model}{reasonSuffix}",
1428
1479
  harvestStatus: "extracting plan state from reasoning\u2026",
1429
1480
  autoEscalation: "\u21E7 auto-escalating to {model} for the rest of this turn \u2014 flash hit {breakdown}. Next turn falls back to {fallback} unless /pro is armed.",
1481
+ readOnlyLoopEscalation: "\u21E7 auto-escalating to {model} \u2014 flash made {n} consecutive read-only calls without producing an edit or final answer. Next turn falls back to {fallback} unless /pro is armed.",
1430
1482
  repeatToolCallWarning: "Caught a repeated tool call \u2014 let the model see the issue and retry with a different approach.",
1431
1483
  stormStuck: "Stopped a stuck retry loop \u2014 the model kept calling the same tool with identical args after a self-correction nudge. Try /retry, rephrase, or rule out the underlying blocker.",
1432
1484
  stormSuppressed: "Suppressed {count} repeated tool call(s) \u2014 same name + args fired 3+ times.",
@@ -1462,6 +1514,7 @@ var EN = {
1462
1514
  basic: {
1463
1515
  newInfo: "\u25B8 new conversation \u2014 dropped {count} message(s) from context. Same session, fresh slate.",
1464
1516
  newInfoArchived: '\u25B8 new conversation \u2014 dropped {count} message(s) from context. Prior transcript archived as "{archived}" (visible under Sessions).',
1517
+ newInfoSystemReloaded: " \xB7 REASONIX.md / project memory reloaded (next turn pays one cache miss)",
1465
1518
  helpTitle: "Commands:",
1466
1519
  helpShellTitle: "Shell shortcut:",
1467
1520
  helpShell: " !<cmd> run <cmd> in the sandbox root; output goes into",
@@ -2113,7 +2166,8 @@ var EN = {
2113
2166
  healthy: "healthy \xB7 {ms}ms",
2114
2167
  slow: "slow \xB7 {ms}ms",
2115
2168
  verySlow: "very slow \xB7 {ms}ms",
2116
- slowToast: "\u26A0 MCP `{name}` slow \xB7 {seconds}s p95 over the last {sampleSize} calls"
2169
+ slowToast: "\u26A0 MCP `{name}` slow \xB7 {seconds}s p95 over the last {sampleSize} calls",
2170
+ emptyHint: "\u2139 no MCP servers configured \u2014 try: `reasonix setup` to re-pick, or `reasonix mcp install filesystem`"
2117
2171
  },
2118
2172
  denyContextInput: {
2119
2173
  description: "Tell the agent why you denied this. The next attempt will see your reason as additional context."
@@ -2237,7 +2291,10 @@ var zhCN = {
2237
2291
  cancel: "\u53D6\u6D88",
2238
2292
  confirm: "\u786E\u8BA4",
2239
2293
  back: "\u8FD4\u56DE",
2240
- next: "\u4E0B\u4E00\u6B65"
2294
+ next: "\u4E0B\u4E00\u6B65",
2295
+ tool: "\u5DE5\u5177",
2296
+ running: "\u8FD0\u884C\u4E2D",
2297
+ noTurns: "(\u6682\u65E0\u5BF9\u8BDD)"
2241
2298
  },
2242
2299
  cli: {
2243
2300
  description: "DeepSeek \u539F\u751F\u667A\u80FD\u4F53\u6846\u67B6 \u2014 \u4E13\u4E3A\u7F13\u5B58\u547D\u4E2D\u548C\u4F4E\u6210\u672C\u4EE4\u724C\u6784\u5EFA\u3002",
@@ -2710,9 +2767,18 @@ var zhCN = {
2710
2767
  counterDone: "{done}/{total} \u5DF2\u5B8C\u6210\uFF08{pct}%\uFF09 \xB7 \u5171 {total} \u6B65",
2711
2768
  counterDoneSingular: "{done}/{total} \u5DF2\u5B8C\u6210\uFF08{pct}%\uFF09 \xB7 \u5171 {total} \u6B65"
2712
2769
  },
2770
+ noPlanSummary: "\u5C1A\u672A\u63D0\u4EA4\u8BA1\u5212\u5185\u5BB9\u3002",
2771
+ detailCollapsedHint: "Ctrl+P \u5C55\u5F00\u5B8C\u6574\u8BA1\u5212\u8BE6\u60C5\u3002",
2772
+ detailExpandedHint: "Ctrl+P \u6536\u8D77\u8BE6\u60C5\u3002",
2773
+ detailHeader: "\u8BA1\u5212\u8BE6\u60C5",
2774
+ detailWindow: "\u663E\u793A\u7B2C {start}-{end} \u884C\uFF0C\u5171 {total} \u884C",
2775
+ detailScrollHint: "PgUp/PgDn \u6EDA\u52A8\u8BE6\u60C5 \xB7 Home/End \u8DF3\u8F6C",
2713
2776
  reviseTitle: "\u4FEE\u6539\u8BA1\u5212",
2714
2777
  reviseSteps: "{count} \u4E2A\u6B65\u9AA4",
2715
- reviseFooter: "\u2191\u2193 \u7126\u70B9 \xB7 \u7A7A\u683C\u5207\u6362\u8DF3\u8FC7 \xB7 k/j \u79FB\u52A8 \xB7 \u23CE \u786E\u8BA4 \xB7 Esc \u53D6\u6D88"
2778
+ reviseFooter: "\u2191\u2193 \u7126\u70B9 \xB7 \u7A7A\u683C\u5207\u6362\u8DF3\u8FC7 \xB7 k/j \u79FB\u52A8 \xB7 \u23CE \u786E\u8BA4 \xB7 Esc \u53D6\u6D88",
2779
+ riskMed: " \u4E2D",
2780
+ riskHigh: " \u9AD8",
2781
+ completeMsg: "\u25B8 \u8BA1\u5212\u5B8C\u6210 \u2014 \u5168\u90E8 {total} \u4E2A\u6B65\u9AA4\u5DF2\u5B8C\u6210 \xB7 \u5DF2\u5F52\u6863"
2716
2782
  },
2717
2783
  app: {
2718
2784
  walkCancelledRemaining: "\u25B8 \u6D4F\u89C8\u5DF2\u53D6\u6D88 \u2014 \u8FD8\u6709 {count} \u4E2A\u5F85\u5904\u7406\u7F16\u8F91\u5757\u3002",
@@ -2732,6 +2798,9 @@ var zhCN = {
2732
2798
  notedVerbAppended: "\u8FFD\u52A0\u5230",
2733
2799
  memoryWriteFailed: "# \u8BB0\u5FC6\u5199\u5165\u5931\u8D25",
2734
2800
  commandFailed: "! \u547D\u4EE4\u5931\u8D25",
2801
+ 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",
2802
+ btwHeader: "\u226B btw",
2803
+ btwFailed: "/btw \u8C03\u7528\u5931\u8D25",
2735
2804
  restoreCodeOnly: "\u25B8 /restore \u4EC5\u5728\u4EE3\u7801\u6A21\u5F0F\u53EF\u7528",
2736
2805
  hookUserPromptSubmit: "UserPromptSubmit \u94A9\u5B50",
2737
2806
  hookStop: "Stop \u94A9\u5B50",
@@ -2774,6 +2843,7 @@ var zhCN = {
2774
2843
  flashEscalation: "\u21E7 flash \u8BF7\u6C42\u5347\u7EA7 \u2014 \u672C\u8F6E\u6539\u7528 {model}{reasonSuffix}",
2775
2844
  harvestStatus: "\u6B63\u5728\u4ECE\u63A8\u7406\u8FC7\u7A0B\u63D0\u53D6\u8BA1\u5212\u72B6\u6001\u2026",
2776
2845
  autoEscalation: "\u21E7 \u672C\u8F6E\u5269\u4F59\u8C03\u7528\u81EA\u52A8\u5347\u7EA7\u5230 {model} \u2014 flash \u547D\u4E2D {breakdown}\u3002\u4E0B\u4E00\u8F6E\u56DE\u9000\u5230 {fallback}\uFF0C\u9664\u975E\u5DF2\u88C5\u5907 /pro\u3002",
2846
+ readOnlyLoopEscalation: "\u21E7 \u81EA\u52A8\u5347\u7EA7\u5230 {model} \u2014 flash \u8FDE\u7EED {n} \u6B21\u53EA\u8BFB\u8C03\u7528\uFF0C\u672A\u4EA7\u51FA\u4FEE\u6539\u6216\u6700\u7EC8\u7B54\u6848\u3002\u4E0B\u4E00\u8F6E\u56DE\u9000\u5230 {fallback}\uFF0C\u9664\u975E\u5DF2\u88C5\u5907 /pro\u3002",
2777
2847
  repeatToolCallWarning: "\u62E6\u622A\u5230\u91CD\u590D\u5DE5\u5177\u8C03\u7528 \u2014 \u8BA9\u6A21\u578B\u5BDF\u89C9\u95EE\u9898\u5E76\u6362\u79CD\u65B9\u5F0F\u91CD\u8BD5\u3002",
2778
2848
  stormStuck: "\u5DF2\u505C\u6B62\u5361\u6B7B\u7684\u91CD\u8BD5\u5FAA\u73AF \u2014 \u6A21\u578B\u5728\u81EA\u7EA0\u63D0\u793A\u540E\u4ECD\u4EE5\u76F8\u540C\u53C2\u6570\u91CD\u590D\u8C03\u7528\u540C\u4E00\u5DE5\u5177\u3002\u8BF7\u5C1D\u8BD5 /retry\u3001\u6362\u79CD\u8BF4\u6CD5\uFF0C\u6216\u6392\u67E5\u5E95\u5C42\u963B\u585E\u3002",
2779
2849
  stormSuppressed: "\u5DF2\u6291\u5236 {count} \u6B21\u91CD\u590D\u5DE5\u5177\u8C03\u7528 \u2014 \u540C\u4E00\u540D\u79F0 + \u53C2\u6570\u89E6\u53D1 3 \u6B21\u4EE5\u4E0A\u3002",
@@ -2809,6 +2879,7 @@ var zhCN = {
2809
2879
  basic: {
2810
2880
  newInfo: "\u25B8 \u65B0\u5BF9\u8BDD \u2014 \u5DF2\u4ECE\u4E0A\u4E0B\u6587\u4E2D\u4E22\u5F03 {count} \u6761\u6D88\u606F\u3002\u540C\u4E00\u4F1A\u8BDD\uFF0C\u5168\u65B0\u5F00\u59CB\u3002",
2811
2881
  newInfoArchived: "\u25B8 \u65B0\u5BF9\u8BDD \u2014 \u5DF2\u4ECE\u4E0A\u4E0B\u6587\u4E2D\u4E22\u5F03 {count} \u6761\u6D88\u606F\u3002\u539F\u5BF9\u8BDD\u5DF2\u5F52\u6863\u4E3A\u300C{archived}\u300D\uFF0C\u53EF\u5728 Sessions \u9762\u677F\u67E5\u770B\u3002",
2882
+ newInfoSystemReloaded: " \xB7 REASONIX.md / \u9879\u76EE\u8BB0\u5FC6\u5DF2\u91CD\u65B0\u52A0\u8F7D\uFF08\u4E0B\u4E00\u8F6E\u4E00\u6B21\u6027 cache miss\uFF09",
2812
2883
  helpTitle: "\u547D\u4EE4\uFF1A",
2813
2884
  helpShellTitle: "Shell \u5FEB\u6377\u65B9\u5F0F\uFF1A",
2814
2885
  helpShell: " !<cmd> \u5728\u6C99\u7BB1\u6839\u76EE\u5F55\u8FD0\u884C <cmd>\uFF1B\u8F93\u51FA\u8FDB\u5165\u5BF9\u8BDD",
@@ -3460,7 +3531,8 @@ var zhCN = {
3460
3531
  healthy: "\u6B63\u5E38 \xB7 {ms}ms",
3461
3532
  slow: "\u7F13\u6162 \xB7 {ms}ms",
3462
3533
  verySlow: "\u975E\u5E38\u6162 \xB7 {ms}ms",
3463
- slowToast: "\u26A0 MCP `{name}` \u54CD\u5E94\u7F13\u6162 \xB7 P95 {seconds}s \xB7 \u6700\u8FD1 {sampleSize} \u6B21\u8C03\u7528"
3534
+ slowToast: "\u26A0 MCP `{name}` \u54CD\u5E94\u7F13\u6162 \xB7 P95 {seconds}s \xB7 \u6700\u8FD1 {sampleSize} \u6B21\u8C03\u7528",
3535
+ 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`"
3464
3536
  },
3465
3537
  denyContextInput: {
3466
3538
  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"
@@ -5184,6 +5256,10 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
5184
5256
  }
5185
5257
 
5186
5258
  // src/loop/healing.ts
5259
+ var _stampSeq = 0;
5260
+ function stampMissingIds(calls) {
5261
+ return calls.map((c) => c.id ? c : { ...c, id: `z-ext-${Date.now()}-${_stampSeq++}` });
5262
+ }
5187
5263
  function fixToolCallPairing(messages) {
5188
5264
  const out = [];
5189
5265
  let droppedAssistantCalls = 0;
@@ -5191,9 +5267,10 @@ function fixToolCallPairing(messages) {
5191
5267
  for (let i = 0; i < messages.length; i++) {
5192
5268
  const msg = messages[i];
5193
5269
  if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
5270
+ const calls = stampMissingIds(msg.tool_calls);
5194
5271
  const needed = /* @__PURE__ */ new Set();
5195
- for (const call of msg.tool_calls) {
5196
- if (call?.id) needed.add(call.id);
5272
+ for (const call of calls) {
5273
+ if (call.id) needed.add(call.id);
5197
5274
  }
5198
5275
  const candidates = [];
5199
5276
  let j = i + 1;
@@ -5207,7 +5284,7 @@ function fixToolCallPairing(messages) {
5207
5284
  j++;
5208
5285
  }
5209
5286
  if (needed.size === 0) {
5210
- out.push(msg);
5287
+ out.push({ ...msg, tool_calls: calls });
5211
5288
  for (const r of candidates) out.push(r);
5212
5289
  i = j - 1;
5213
5290
  } else {
@@ -5271,6 +5348,32 @@ function* hookWarnings(outcomes, turn) {
5271
5348
  }
5272
5349
  }
5273
5350
 
5351
+ // src/loop/read-only-loop-tracker.ts
5352
+ var READONLY_LOOP_ESCALATION_THRESHOLD = 8;
5353
+ var ReadOnlyLoopTracker = class {
5354
+ streak = 0;
5355
+ threshold;
5356
+ constructor(threshold = READONLY_LOOP_ESCALATION_THRESHOLD) {
5357
+ this.threshold = Math.max(1, threshold);
5358
+ }
5359
+ reset() {
5360
+ this.streak = 0;
5361
+ }
5362
+ /** True ONLY on the call where the streak crosses the configured threshold. */
5363
+ noteAndCrossedThreshold(isReadOnly) {
5364
+ if (!isReadOnly) {
5365
+ this.streak = 0;
5366
+ return false;
5367
+ }
5368
+ const before = this.streak;
5369
+ this.streak += 1;
5370
+ return before < this.threshold && this.streak >= this.threshold;
5371
+ }
5372
+ get currentStreak() {
5373
+ return this.streak;
5374
+ }
5375
+ };
5376
+
5274
5377
  // src/loop/turn-failure-tracker.ts
5275
5378
  var FAILURE_ESCALATION_THRESHOLD = 3;
5276
5379
  var TurnFailureTracker = class {
@@ -5310,17 +5413,25 @@ var TurnFailureTracker = class {
5310
5413
  // src/memory/runtime.ts
5311
5414
  import { createHash } from "crypto";
5312
5415
  var ImmutablePrefix = class {
5416
+ /** Stable across turns; rebuilt only on /new when REASONIX.md changed on disk. */
5313
5417
  system;
5314
5418
  /** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
5315
5419
  _toolSpecs;
5316
5420
  fewShots;
5317
- /** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
5421
+ /** Invalidated by addTool / removeTool / replaceSystem; bypassing any of those leaves cache stale → fingerprint diverges from sent prefix. */
5318
5422
  _fingerprintCache = null;
5319
5423
  constructor(opts) {
5320
5424
  this.system = opts.system;
5321
5425
  this._toolSpecs = [...opts.toolSpecs ?? []];
5322
5426
  this.fewShots = Object.freeze([...opts.fewShots ?? []]);
5323
5427
  }
5428
+ /** Replaces the system prompt; returns true iff the string actually changed. Caller must accept a cache miss on the next turn. */
5429
+ replaceSystem(s) {
5430
+ if (this.system === s) return false;
5431
+ this.system = s;
5432
+ this._fingerprintCache = null;
5433
+ return true;
5434
+ }
5324
5435
  get toolSpecs() {
5325
5436
  return this._toolSpecs;
5326
5437
  }
@@ -5751,6 +5862,7 @@ var CacheFirstLoop = class {
5751
5862
  confirmationGate;
5752
5863
  /** Number of messages that were pre-loaded from the session file. */
5753
5864
  resumedMessageCount;
5865
+ _rebuildSystem;
5754
5866
  _turn = 0;
5755
5867
  _streamPreference;
5756
5868
  /** Threaded through HTTP + every tool dispatch so Esc cancels in-flight work, not after. */
@@ -5760,6 +5872,7 @@ var CacheFirstLoop = class {
5760
5872
  _proArmedForNextTurn = false;
5761
5873
  _escalateThisTurn = false;
5762
5874
  _turnFailures;
5875
+ _readOnlyLoop;
5763
5876
  _turnSelfCorrected = false;
5764
5877
  _foldedThisTurn = false;
5765
5878
  _toolDispatchesThisStep = 0;
@@ -5782,34 +5895,18 @@ var CacheFirstLoop = class {
5782
5895
  this._turnFailures = new TurnFailureTracker(
5783
5896
  resolveFailureThreshold(opts.failureThreshold, FAILURE_ESCALATION_THRESHOLD)
5784
5897
  );
5898
+ this._readOnlyLoop = new ReadOnlyLoopTracker(
5899
+ parsePositiveIntEnv(process.env.REASONIX_READONLY_LOOP_THRESHOLD) ?? READONLY_LOOP_ESCALATION_THRESHOLD
5900
+ );
5785
5901
  this.maxToolIters = opts.maxToolIters ?? 64;
5786
5902
  this.hooks = opts.hooks ?? [];
5787
5903
  this.hookCwd = opts.hookCwd ?? process.cwd();
5788
5904
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
5905
+ this._rebuildSystem = opts.rebuildSystem ?? null;
5789
5906
  this._streamPreference = opts.stream ?? true;
5790
5907
  this.stream = this._streamPreference;
5791
5908
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
5792
5909
  const registry = this.tools;
5793
- const isMutating = (call) => {
5794
- const name = call.function?.name;
5795
- if (!name) return false;
5796
- const def = registry.get(name);
5797
- if (!def) return false;
5798
- if (def.readOnlyCheck) {
5799
- let args = {};
5800
- try {
5801
- args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
5802
- } catch {
5803
- }
5804
- try {
5805
- if (def.readOnlyCheck(args)) return false;
5806
- } catch (err) {
5807
- process.stderr.write(`readOnlyCheck for ${name} threw: ${err.message}
5808
- `);
5809
- }
5810
- }
5811
- return def.readOnly !== true;
5812
- };
5813
5910
  const isStormExempt = (call) => {
5814
5911
  const name = call.function?.name;
5815
5912
  if (!name) return false;
@@ -5817,7 +5914,7 @@ var CacheFirstLoop = class {
5817
5914
  };
5818
5915
  this.repair = new ToolCallRepair({
5819
5916
  allowedToolNames: allowedNames,
5820
- isMutating,
5917
+ isMutating: (call) => this.isMutating(call),
5821
5918
  isStormExempt,
5822
5919
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
5823
5920
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
@@ -5910,7 +6007,7 @@ var CacheFirstLoop = class {
5910
6007
  }
5911
6008
  }
5912
6009
  }
5913
- /** "New chat" — drops in-memory messages, archives the on-disk transcript so it survives in Sessions, keeps sessionName so the prefix cache stays warm. */
6010
+ /** "New chat" — drops in-memory messages, archives the on-disk transcript so it survives in Sessions, keeps sessionName so the prefix cache stays warm. Re-runs the system-prompt builder if one was wired (issue #778: REASONIX.md edits otherwise need a restart). */
5914
6011
  clearLog() {
5915
6012
  const dropped = this.log.length;
5916
6013
  this.log.compactInPlace([]);
@@ -5924,7 +6021,14 @@ var CacheFirstLoop = class {
5924
6021
  }
5925
6022
  this.scratch.reset();
5926
6023
  this._inflight.clear();
5927
- return { dropped, archived };
6024
+ let systemRebuilt = false;
6025
+ if (this._rebuildSystem) {
6026
+ try {
6027
+ systemRebuilt = this.prefix.replaceSystem(this._rebuildSystem());
6028
+ } catch {
6029
+ }
6030
+ }
6031
+ return { dropped, archived, systemRebuilt };
5928
6032
  }
5929
6033
  configure(opts) {
5930
6034
  if (opts.model !== void 0) this.model = opts.model;
@@ -5970,6 +6074,35 @@ var CacheFirstLoop = class {
5970
6074
  this._escalateThisTurn = true;
5971
6075
  return true;
5972
6076
  }
6077
+ /** Returns true ONLY on the call where the read-only streak crosses the threshold (#681). */
6078
+ noteReadOnlyToolCall(call) {
6079
+ const isReadOnly = !this.isMutating(call);
6080
+ if (!this._readOnlyLoop.noteAndCrossedThreshold(isReadOnly)) return false;
6081
+ if (this._escalateThisTurn || !this.autoEscalate) return false;
6082
+ this._escalateThisTurn = true;
6083
+ return true;
6084
+ }
6085
+ /** A call counts as mutating when its definition reports `readOnly !== true` and any dynamic `readOnlyCheck` doesn't override that for these args. */
6086
+ isMutating(call) {
6087
+ const name = call.function?.name;
6088
+ if (!name) return false;
6089
+ const def = this.tools.get(name);
6090
+ if (!def) return false;
6091
+ if (def.readOnlyCheck) {
6092
+ let args = {};
6093
+ try {
6094
+ args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
6095
+ } catch {
6096
+ }
6097
+ try {
6098
+ if (def.readOnlyCheck(args)) return false;
6099
+ } catch (err) {
6100
+ process.stderr.write(`readOnlyCheck for ${name} threw: ${err.message}
6101
+ `);
6102
+ }
6103
+ }
6104
+ return def.readOnly !== true;
6105
+ }
5973
6106
  async runOneToolCall(call, signal) {
5974
6107
  const name = call.function?.name ?? "";
5975
6108
  const args = call.function?.arguments ?? "{}";
@@ -6090,6 +6223,7 @@ ${reason}`
6090
6223
  this.scratch.reset();
6091
6224
  this.repair.resetStorm();
6092
6225
  this._turnFailures.reset();
6226
+ this._readOnlyLoop.reset();
6093
6227
  this._turnSelfCorrected = false;
6094
6228
  this._escalateThisTurn = false;
6095
6229
  this._foldedThisTurn = false;
@@ -6516,6 +6650,17 @@ ${reason}`
6516
6650
  })
6517
6651
  };
6518
6652
  }
6653
+ if (this.noteReadOnlyToolCall(call)) {
6654
+ yield {
6655
+ turn: this._turn,
6656
+ role: "warning",
6657
+ content: t("loop.readOnlyLoopEscalation", {
6658
+ model: ESCALATION_MODEL,
6659
+ n: this._readOnlyLoop.currentStreak,
6660
+ fallback: this.model
6661
+ })
6662
+ };
6663
+ }
6519
6664
  yield {
6520
6665
  turn: this._turn,
6521
6666
  role: "tool",
@@ -6844,7 +6989,7 @@ function parseAtQuery(query) {
6844
6989
  trailingSlash: false
6845
6990
  };
6846
6991
  }
6847
- var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
6992
+ var AT_PICKER_PREFIX = /(?:^|\s)@([\p{L}\p{N}_./\\-]*)$/u;
6848
6993
  function detectAtPicker(input) {
6849
6994
  const m = AT_PICKER_PREFIX.exec(input);
6850
6995
  if (!m) return null;
@@ -6930,7 +7075,7 @@ function fuzzySubseqScore(needle, target) {
6930
7075
  const lengthPenalty = Math.floor(target.length / 4);
6931
7076
  return quality + lengthPenalty;
6932
7077
  }
6933
- var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
7078
+ var AT_MENTION_PATTERN = /(?<=^|\s)@([\p{L}\p{N}_./\\-]+)/gu;
6934
7079
  function expandAtMentions(text, rootDir, opts = {}) {
6935
7080
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
6936
7081
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
@@ -7620,16 +7765,24 @@ function ensureDir(p) {
7620
7765
  if (!existsSync7(p)) mkdirSync4(p, { recursive: true });
7621
7766
  }
7622
7767
  function formatFrontmatter(e) {
7623
- return [
7768
+ const lines = [
7624
7769
  "---",
7625
7770
  `name: ${e.name}`,
7626
7771
  `description: ${e.description.replace(/\n/g, " ")}`,
7627
7772
  `type: ${e.type}`,
7628
7773
  `scope: ${e.scope}`,
7629
- `created: ${e.createdAt}`,
7630
- "---",
7631
- ""
7632
- ].join("\n");
7774
+ `created: ${e.createdAt}`
7775
+ ];
7776
+ if (e.priority) lines.push(`priority: ${e.priority}`);
7777
+ if (e.expires) lines.push(`expires: ${e.expires}`);
7778
+ lines.push("---", "");
7779
+ return lines.join("\n");
7780
+ }
7781
+ function coercePriority(v) {
7782
+ return v === "low" || v === "medium" || v === "high" ? v : void 0;
7783
+ }
7784
+ function coerceExpires(v) {
7785
+ return v === "project_end" ? v : void 0;
7633
7786
  }
7634
7787
  function todayIso() {
7635
7788
  const d = /* @__PURE__ */ new Date();
@@ -7691,7 +7844,7 @@ var MemoryStore = class {
7691
7844
  }
7692
7845
  const raw = readFileSync9(file, "utf8");
7693
7846
  const { data, body } = parseFrontmatter(raw);
7694
- return {
7847
+ const entry = {
7695
7848
  name: data.name ?? name,
7696
7849
  type: data.type ?? "project",
7697
7850
  scope: data.scope ?? scope,
@@ -7699,6 +7852,11 @@ var MemoryStore = class {
7699
7852
  body: body.trim(),
7700
7853
  createdAt: data.created ?? ""
7701
7854
  };
7855
+ const priority = coercePriority(data.priority);
7856
+ if (priority) entry.priority = priority;
7857
+ const expires = coerceExpires(data.expires);
7858
+ if (expires) entry.expires = expires;
7859
+ return entry;
7702
7860
  }
7703
7861
  /** Skips malformed files — index stays queryable even if one file is hand-edited into nonsense. */
7704
7862
  list() {
@@ -7741,6 +7899,8 @@ var MemoryStore = class {
7741
7899
  body,
7742
7900
  createdAt: todayIso()
7743
7901
  };
7902
+ if (input.priority) entry.priority = input.priority;
7903
+ if (input.expires) entry.expires = input.expires;
7744
7904
  const dir = this.dir(input.scope);
7745
7905
  const file = join8(dir, `${name}.md`);
7746
7906
  const content = `${formatFrontmatter(entry)}${body}
@@ -7824,13 +7984,36 @@ function applyGlobalReasonixMemory(basePrompt, homeDir) {
7824
7984
  "```"
7825
7985
  ].join("\n");
7826
7986
  }
7987
+ function effectivePriority(entry, cfg) {
7988
+ if (entry.priority) return entry.priority;
7989
+ return memoryTypeDefaults(entry.type, cfg).priority;
7990
+ }
7991
+ function highPriorityBlock(entries, cfg) {
7992
+ const high = entries.filter((e) => effectivePriority(e, cfg) === "high");
7993
+ if (high.length === 0) return null;
7994
+ const lines = [
7995
+ "# HIGH PRIORITY constraints (must observe)",
7996
+ "",
7997
+ "These memories were declared `priority: high` (via config.memory.customTypes or the memory file itself). Treat them as hard rules \u2014 violations override any other guidance below.",
7998
+ ""
7999
+ ];
8000
+ for (const e of high) {
8001
+ const head = `!!! [${e.scope}/${e.type}/${e.name}] ${e.description || "(no description)"}`;
8002
+ lines.push(head);
8003
+ if (e.body) lines.push("", e.body);
8004
+ lines.push("");
8005
+ }
8006
+ return lines.join("\n").trimEnd();
8007
+ }
7827
8008
  function applyUserMemory(basePrompt, opts = {}) {
7828
8009
  if (!memoryEnabled()) return basePrompt;
7829
8010
  const store = new MemoryStore(opts);
7830
8011
  const global = store.loadIndex("global");
7831
8012
  const project = store.hasProjectScope() ? store.loadIndex("project") : null;
7832
- if (!global && !project) return basePrompt;
8013
+ const high = highPriorityBlock(store.list(), opts.cfg);
8014
+ if (!global && !project && !high) return basePrompt;
7833
8015
  const parts = [basePrompt];
8016
+ if (high) parts.push("", high);
7834
8017
  if (global) {
7835
8018
  parts.push(
7836
8019
  "",
@@ -7866,7 +8049,7 @@ function applyMemoryStack(basePrompt, rootDir) {
7866
8049
 
7867
8050
  // src/tools/filesystem.ts
7868
8051
  import { promises as fs4 } from "fs";
7869
- import * as pathMod4 from "path";
8052
+ import * as pathMod5 from "path";
7870
8053
  import picomatch3 from "picomatch";
7871
8054
 
7872
8055
  // src/tools/fs/edit.ts
@@ -8088,15 +8271,149 @@ async function globFiles(ctx, startAbs, args) {
8088
8271
  return lines.join("\n");
8089
8272
  }
8090
8273
 
8274
+ // src/tools/fs/outline.ts
8275
+ import * as pathMod3 from "path";
8276
+ var OUTLINE_MAX_ENTRIES = 30;
8277
+ var OUTLINE_TAIL_KEEP = 5;
8278
+ var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
8279
+ var PY_DECL_RE = /^(?:async\s+)?(def|class)\s+(\w+)/;
8280
+ var GO_DECL_RE = /^(func|type|var|const)\s+(?:\([^)]+\)\s+)?(\w+)/;
8281
+ var RUST_DECL_RE = /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|mod|type|const|static|union)\s+(\w+)/;
8282
+ var RUST_IMPL_RE = /^(?:unsafe\s+)?impl(?:\s*<[^>]+>)?\s+(?:[^{]+\s+for\s+)?(\w+)/;
8283
+ var MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
8284
+ var MD_FENCE_RE = /^```/;
8285
+ var EXT_TO_LANG = {
8286
+ ".ts": "ts",
8287
+ ".tsx": "ts",
8288
+ ".mts": "ts",
8289
+ ".cts": "ts",
8290
+ ".js": "ts",
8291
+ ".jsx": "ts",
8292
+ ".mjs": "ts",
8293
+ ".cjs": "ts",
8294
+ ".py": "py",
8295
+ ".pyi": "py",
8296
+ ".go": "go",
8297
+ ".rs": "rust",
8298
+ ".md": "md",
8299
+ ".markdown": "md",
8300
+ ".mdx": "md"
8301
+ };
8302
+ function extractOutline(filename, lines) {
8303
+ const ext = pathMod3.extname(filename).toLowerCase();
8304
+ const lang = EXT_TO_LANG[ext];
8305
+ if (!lang) return [];
8306
+ switch (lang) {
8307
+ case "ts":
8308
+ return extractTs(lines);
8309
+ case "py":
8310
+ return extractPython(lines);
8311
+ case "go":
8312
+ return extractGo(lines);
8313
+ case "rust":
8314
+ return extractRust(lines);
8315
+ case "md":
8316
+ return extractMarkdown(lines);
8317
+ }
8318
+ }
8319
+ function extractTs(lines) {
8320
+ const out = [];
8321
+ for (let i = 0; i < lines.length; i++) {
8322
+ const line = lines[i];
8323
+ if (!line.startsWith("export ")) continue;
8324
+ const m = TS_EXPORT_RE.exec(line);
8325
+ if (!m) continue;
8326
+ out.push({ line: i + 1, text: `export ${m[1]} ${m[2]}` });
8327
+ }
8328
+ return out;
8329
+ }
8330
+ function extractPython(lines) {
8331
+ const out = [];
8332
+ for (let i = 0; i < lines.length; i++) {
8333
+ const line = lines[i];
8334
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
8335
+ const m = PY_DECL_RE.exec(line);
8336
+ if (!m) continue;
8337
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8338
+ }
8339
+ return out;
8340
+ }
8341
+ function extractGo(lines) {
8342
+ const out = [];
8343
+ for (let i = 0; i < lines.length; i++) {
8344
+ const line = lines[i];
8345
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
8346
+ const m = GO_DECL_RE.exec(line);
8347
+ if (!m) continue;
8348
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8349
+ }
8350
+ return out;
8351
+ }
8352
+ function extractRust(lines) {
8353
+ const out = [];
8354
+ for (let i = 0; i < lines.length; i++) {
8355
+ const line = lines[i];
8356
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
8357
+ const implMatch = RUST_IMPL_RE.exec(line);
8358
+ if (implMatch) {
8359
+ out.push({ line: i + 1, text: `impl ${implMatch[1]}` });
8360
+ continue;
8361
+ }
8362
+ const m = RUST_DECL_RE.exec(line);
8363
+ if (!m) continue;
8364
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8365
+ }
8366
+ return out;
8367
+ }
8368
+ function extractMarkdown(lines) {
8369
+ const out = [];
8370
+ let inFence = false;
8371
+ for (let i = 0; i < lines.length; i++) {
8372
+ const line = lines[i];
8373
+ if (MD_FENCE_RE.test(line)) {
8374
+ inFence = !inFence;
8375
+ continue;
8376
+ }
8377
+ if (inFence) continue;
8378
+ const m = MD_HEADING_RE.exec(line);
8379
+ if (!m) continue;
8380
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8381
+ }
8382
+ return out;
8383
+ }
8384
+ function formatOutline(entries) {
8385
+ const total = entries.length;
8386
+ if (total === 0) return "";
8387
+ const lastEntry = entries[total - 1];
8388
+ const width = String(lastEntry.line).length;
8389
+ const fmt = (e) => ` L${String(e.line).padStart(width, " ")} ${e.text}`;
8390
+ const header = `[outline: ${total} symbol${total === 1 ? "" : "s"}]`;
8391
+ if (total <= OUTLINE_MAX_ENTRIES) {
8392
+ return [header, ...entries.map(fmt)].join("\n");
8393
+ }
8394
+ const headCount = OUTLINE_MAX_ENTRIES - OUTLINE_TAIL_KEEP;
8395
+ const headEntries = entries.slice(0, headCount);
8396
+ const tailEntries = entries.slice(-OUTLINE_TAIL_KEEP);
8397
+ const omitted = total - OUTLINE_MAX_ENTRIES;
8398
+ const gapStart = headEntries[headEntries.length - 1].line;
8399
+ const gapEnd = tailEntries[0].line;
8400
+ return [
8401
+ header,
8402
+ ...headEntries.map(fmt),
8403
+ ` [\u2026 ${omitted} more symbol${omitted === 1 ? "" : "s"} between L${gapStart} and L${gapEnd} \u2026]`,
8404
+ ...tailEntries.map(fmt)
8405
+ ].join("\n");
8406
+ }
8407
+
8091
8408
  // src/tools/fs/search.ts
8092
8409
  import { promises as fs3 } from "fs";
8093
- import * as pathMod3 from "path";
8410
+ import * as pathMod4 from "path";
8094
8411
  function throwIfAborted(signal) {
8095
8412
  if (!signal?.aborted) return;
8096
8413
  throw new DOMException("search aborted by user", "AbortError");
8097
8414
  }
8098
8415
  function displayRel3(rootDir, full) {
8099
- return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
8416
+ return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
8100
8417
  }
8101
8418
  async function searchFiles(ctx, startAbs, args) {
8102
8419
  throwIfAborted(args.signal);
@@ -8120,7 +8437,7 @@ async function searchFiles(ctx, startAbs, args) {
8120
8437
  }
8121
8438
  for (const e of entries) {
8122
8439
  throwIfAborted(args.signal);
8123
- const full = pathMod3.join(dir, e.name);
8440
+ const full = pathMod4.join(dir, e.name);
8124
8441
  const lower = e.name.toLowerCase();
8125
8442
  const hit = re ? re.test(e.name) : lower.includes(needle);
8126
8443
  if (hit) {
@@ -8199,11 +8516,11 @@ async function searchContent(ctx, startAbs, args) {
8199
8516
  throwIfAborted(args.signal);
8200
8517
  if (e.isDirectory()) {
8201
8518
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
8202
- await walk2(pathMod3.join(dir, e.name));
8519
+ await walk2(pathMod4.join(dir, e.name));
8203
8520
  continue;
8204
8521
  }
8205
8522
  if (!e.isFile()) continue;
8206
- const full = pathMod3.join(dir, e.name);
8523
+ const full = pathMod4.join(dir, e.name);
8207
8524
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel3(ctx.rootDir, full))) continue;
8208
8525
  if (ctx.isBinaryByName(e.name)) continue;
8209
8526
  let fh;
@@ -8300,47 +8617,11 @@ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
8300
8617
  var DEFAULT_AUTO_PREVIEW_LINES = 200;
8301
8618
  var AUTO_PREVIEW_HEAD_LINES = 80;
8302
8619
  var AUTO_PREVIEW_TAIL_LINES = 40;
8303
- var OUTLINE_MAX_ENTRIES = 30;
8304
- var OUTLINE_TAIL_KEEP = 5;
8305
- var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
8306
- function extractTsExportOutline(lines) {
8307
- const out = [];
8308
- for (let i = 0; i < lines.length; i++) {
8309
- const line = lines[i];
8310
- if (!line.startsWith("export ")) continue;
8311
- const m = TS_EXPORT_RE.exec(line);
8312
- if (!m) continue;
8313
- out.push({ line: i + 1, kind: m[1], name: m[2] });
8314
- }
8315
- return out;
8316
- }
8317
- function formatOutline(entries) {
8318
- const total = entries.length;
8319
- if (total === 0) return "";
8320
- const lastEntry = entries[total - 1];
8321
- const width = String(lastEntry.line).length;
8322
- const fmt = (e) => ` L${String(e.line).padStart(width, " ")} export ${e.kind} ${e.name}`;
8323
- const header = `[outline: ${total} top-level export${total === 1 ? "" : "s"}]`;
8324
- if (total <= OUTLINE_MAX_ENTRIES) {
8325
- return [header, ...entries.map(fmt)].join("\n");
8326
- }
8327
- const headCount = OUTLINE_MAX_ENTRIES - OUTLINE_TAIL_KEEP;
8328
- const headEntries = entries.slice(0, headCount);
8329
- const tailEntries = entries.slice(-OUTLINE_TAIL_KEEP);
8330
- const omitted = total - OUTLINE_MAX_ENTRIES;
8331
- const gapStart = headEntries[headEntries.length - 1].line;
8332
- const gapEnd = tailEntries[0].line;
8333
- return [
8334
- header,
8335
- ...headEntries.map(fmt),
8336
- ` [\u2026 ${omitted} more export${omitted === 1 ? "" : "s"} between L${gapStart} and L${gapEnd} \u2026]`,
8337
- ...tailEntries.map(fmt)
8338
- ].join("\n");
8339
- }
8620
+ var OUTLINE_MAX_ENTRIES2 = 30;
8340
8621
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
8341
8622
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
8342
8623
  function displayRel4(rootDir, full) {
8343
- return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
8624
+ return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
8344
8625
  }
8345
8626
  var GLOB_METACHARS = /[*?{[]/;
8346
8627
  function compileNameFilter(filter) {
@@ -8359,16 +8640,16 @@ function isLikelyBinaryByName(name) {
8359
8640
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
8360
8641
  }
8361
8642
  function registerFilesystemTools(registry, opts) {
8362
- const rootDir = pathMod4.resolve(opts.rootDir);
8643
+ const rootDir = pathMod5.resolve(opts.rootDir);
8363
8644
  const allowWriting = opts.allowWriting !== false;
8364
8645
  const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
8365
8646
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
8366
- const normRoot = pathMod4.resolve(rootDir);
8647
+ const normRoot = pathMod5.resolve(rootDir);
8367
8648
  const sessionApproved = /* @__PURE__ */ new Set();
8368
8649
  const inflightGate = /* @__PURE__ */ new Map();
8369
8650
  function pathIsUnder(child, parent) {
8370
- const rel = pathMod4.relative(parent, child);
8371
- return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
8651
+ const rel = pathMod5.relative(parent, child);
8652
+ return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
8372
8653
  }
8373
8654
  function looksLikeAbsoluteSystemPath(raw) {
8374
8655
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -8384,7 +8665,7 @@ function registerFilesystemTools(registry, opts) {
8384
8665
  if (pathIsUnder(abs, dir)) return;
8385
8666
  }
8386
8667
  const stat2 = await safeLstat(abs);
8387
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod4.dirname(abs);
8668
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod5.dirname(abs);
8388
8669
  let pending = inflightGate.get(allowPrefix);
8389
8670
  if (!pending) {
8390
8671
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -8412,7 +8693,7 @@ function registerFilesystemTools(registry, opts) {
8412
8693
  throw new Error("path must be a non-empty string");
8413
8694
  }
8414
8695
  if (looksLikeAbsoluteSystemPath(raw)) {
8415
- const abs = pathMod4.resolve(raw);
8696
+ const abs = pathMod5.resolve(raw);
8416
8697
  if (pathIsUnder(abs, normRoot)) return abs;
8417
8698
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
8418
8699
  return abs;
@@ -8422,7 +8703,7 @@ function registerFilesystemTools(registry, opts) {
8422
8703
  normalized = normalized.slice(1);
8423
8704
  }
8424
8705
  if (normalized.length === 0) normalized = ".";
8425
- const resolved = pathMod4.resolve(rootDir, normalized);
8706
+ const resolved = pathMod5.resolve(rootDir, normalized);
8426
8707
  if (!pathIsUnder(resolved, normRoot)) {
8427
8708
  throw new Error(
8428
8709
  `path escapes sandbox root (${normRoot}): ${raw} \u2014 use an absolute system path like /Users/foo or C:\\Users\\foo to request approved outside-sandbox access`
@@ -8444,7 +8725,7 @@ function registerFilesystemTools(registry, opts) {
8444
8725
  - head: N \u2192 first N lines (imports, public API, small configs)
8445
8726
  - tail: N \u2192 last N lines (recently-added code, log tails)
8446
8727
  - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
8447
- When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_LINES} lines, the tool auto-returns a head+tail preview with an "N lines omitted" marker, plus a top-level export outline (function / class / const / interface / type / enum names with line numbers, capped at ${OUTLINE_MAX_ENTRIES}) so you can pick a smart range without a follow-up grep. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first only when the outline doesn't have what you want \u2014 one scoped read beats three full-file reads.`,
8728
+ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_LINES} lines, the tool auto-returns a head+tail preview with an "N lines omitted" marker, plus a top-level symbol outline (TS/JS exports, Python def/class, Go func/type, Rust fn/struct/impl/trait, Markdown headings, with line numbers, capped at ${OUTLINE_MAX_ENTRIES2}) so you can pick a smart range without a follow-up grep. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first only when the outline doesn't have what you want \u2014 one scoped read beats three full-file reads.`,
8448
8729
  readOnly: true,
8449
8730
  stormExempt: true,
8450
8731
  parameters: {
@@ -8512,7 +8793,7 @@ ${slice.join("\n")}`;
8512
8793
  const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
8513
8794
  const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
8514
8795
  const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
8515
- const outline = formatOutline(extractTsExportOutline(lines));
8796
+ const outline = formatOutline(extractOutline(abs, lines));
8516
8797
  const parts = [
8517
8798
  `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
8518
8799
  head
@@ -8620,7 +8901,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8620
8901
  lines.push(line);
8621
8902
  emitted++;
8622
8903
  if (e.isDirectory() && !skip) {
8623
- await walk2(pathMod4.join(dir, e.name), depth + 1);
8904
+ await walk2(pathMod5.join(dir, e.name), depth + 1);
8624
8905
  }
8625
8906
  }
8626
8907
  };
@@ -8780,7 +9061,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8780
9061
  },
8781
9062
  fn: async (args, ctx) => {
8782
9063
  const abs = await safePath(args.path, "write_file", ctx, "write");
8783
- await fs4.mkdir(pathMod4.dirname(abs), { recursive: true });
9064
+ await fs4.mkdir(pathMod5.dirname(abs), { recursive: true });
8784
9065
  await fs4.writeFile(abs, args.content, "utf8");
8785
9066
  return `wrote ${args.content.length} chars to ${displayRel4(rootDir, abs)}`;
8786
9067
  }
@@ -8866,7 +9147,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8866
9147
  fn: async (args, ctx) => {
8867
9148
  const src = await safePath(args.source, "move_file", ctx, "write");
8868
9149
  const dst = await safePath(args.destination, "move_file", ctx, "write");
8869
- await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
9150
+ await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
8870
9151
  await fs4.rename(src, dst);
8871
9152
  return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
8872
9153
  }
@@ -8934,7 +9215,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8934
9215
  fn: async (args, ctx) => {
8935
9216
  const src = await safePath(args.source, "copy_file", ctx);
8936
9217
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
8937
- await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
9218
+ await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
8938
9219
  await fs4.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
8939
9220
  return `copied ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
8940
9221
  }
@@ -8946,6 +9227,16 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8946
9227
  function registerMemoryTools(registry, opts = {}) {
8947
9228
  const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
8948
9229
  const hasProject = store.hasProjectScope();
9230
+ const registry_types = loadMemoryTypeRegistry();
9231
+ const customTypeNames = registry_types.filter((r) => !r.builtin).map((r) => r.name);
9232
+ const typeDescParts = [
9233
+ "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
9234
+ ];
9235
+ if (customTypeNames.length > 0) {
9236
+ typeDescParts.push(
9237
+ `Custom types declared in config: ${customTypeNames.join(", ")}. Any string is accepted; unknown types are stored verbatim and treated as 'reference' priority.`
9238
+ );
9239
+ }
8949
9240
  registry.register({
8950
9241
  name: "remember",
8951
9242
  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.",
@@ -8954,8 +9245,7 @@ function registerMemoryTools(registry, opts = {}) {
8954
9245
  properties: {
8955
9246
  type: {
8956
9247
  type: "string",
8957
- enum: ["user", "feedback", "project", "reference"],
8958
- description: "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
9248
+ description: typeDescParts.join(" ")
8959
9249
  },
8960
9250
  scope: {
8961
9251
  type: "string",
@@ -8973,6 +9263,16 @@ function registerMemoryTools(registry, opts = {}) {
8973
9263
  content: {
8974
9264
  type: "string",
8975
9265
  description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
9266
+ },
9267
+ priority: {
9268
+ type: "string",
9269
+ enum: ["low", "medium", "high"],
9270
+ 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."
9271
+ },
9272
+ expires: {
9273
+ type: "string",
9274
+ enum: ["project_end"],
9275
+ description: "Optional lifecycle hint. `project_end` causes `/memory clear project` to also remove this entry even when it's stored at global scope."
8976
9276
  }
8977
9277
  },
8978
9278
  required: ["type", "scope", "name", "description", "content"]
@@ -8989,7 +9289,9 @@ function registerMemoryTools(registry, opts = {}) {
8989
9289
  type: args.type,
8990
9290
  scope: args.scope,
8991
9291
  description: args.description,
8992
- body: args.content
9292
+ body: args.content,
9293
+ ...args.priority ? { priority: args.priority } : {},
9294
+ ...args.expires ? { expires: args.expires } : {}
8993
9295
  });
8994
9296
  const key = sanitizeMemoryName(args.name);
8995
9297
  return [
@@ -9932,11 +10234,11 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
9932
10234
  }
9933
10235
 
9934
10236
  // src/tools/shell.ts
9935
- import * as pathMod8 from "path";
10237
+ import * as pathMod9 from "path";
9936
10238
 
9937
10239
  // src/tools/jobs.ts
9938
10240
  import { spawn as spawn2 } from "child_process";
9939
- import * as pathMod5 from "path";
10241
+ import * as pathMod6 from "path";
9940
10242
  function killProcessTree(pid, signal) {
9941
10243
  if (process.platform === "win32") {
9942
10244
  const args = ["/pid", String(pid), "/T"];
@@ -9996,7 +10298,7 @@ var JobRegistry = class {
9996
10298
  const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
9997
10299
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
9998
10300
  const spawnOpts = {
9999
- cwd: pathMod5.resolve(opts.cwd),
10301
+ cwd: pathMod6.resolve(opts.cwd),
10000
10302
  shell: false,
10001
10303
  windowsHide: true,
10002
10304
  env: process.env,
@@ -10109,12 +10411,15 @@ ${job.output.slice(start)}`;
10109
10411
  job.signalReady();
10110
10412
  job.signalClosed();
10111
10413
  });
10112
- child.on("close", (code) => {
10414
+ const settleClosed = (code) => {
10415
+ if (!job.running && job.exitCode !== null) return;
10113
10416
  job.running = false;
10114
10417
  job.exitCode = code;
10115
10418
  job.signalReady();
10116
10419
  job.signalClosed();
10117
- });
10420
+ };
10421
+ child.on("exit", settleClosed);
10422
+ child.on("close", settleClosed);
10118
10423
  const onAbort = () => this.stop(id, { graceMs: 100 });
10119
10424
  if (opts.signal?.aborted) {
10120
10425
  onAbort();
@@ -10171,21 +10476,26 @@ ${job.output.slice(start)}`;
10171
10476
  latestOutput: job.output
10172
10477
  };
10173
10478
  }
10174
- const timeoutMs = Math.max(0, Math.min(3e4, opts.timeoutMs ?? 5e3));
10479
+ const timeoutMs = Math.max(0, Math.min(3e5, opts.timeoutMs ?? 5e3));
10480
+ const waitFor = opts.waitFor ?? "exit";
10175
10481
  const startOutput = job.output;
10482
+ const racers = [job.closedPromise];
10176
10483
  let wakeOutput = null;
10177
- const outputPromise = new Promise((resolve10) => {
10178
- wakeOutput = resolve10;
10179
- job.outputWaiters.add(resolve10);
10180
- });
10484
+ if (waitFor === "output-or-exit") {
10485
+ racers.push(
10486
+ new Promise((resolve10) => {
10487
+ wakeOutput = resolve10;
10488
+ job.outputWaiters.add(resolve10);
10489
+ })
10490
+ );
10491
+ }
10181
10492
  let timer = null;
10182
- await Promise.race([
10183
- job.closedPromise,
10184
- outputPromise,
10493
+ racers.push(
10185
10494
  new Promise((resolve10) => {
10186
10495
  timer = setTimeout(resolve10, timeoutMs);
10187
10496
  })
10188
- ]);
10497
+ );
10498
+ await Promise.race(racers);
10189
10499
  if (timer) clearTimeout(timer);
10190
10500
  if (wakeOutput) job.outputWaiters.delete(wakeOutput);
10191
10501
  return {
@@ -10219,6 +10529,10 @@ ${job.output.slice(start)}`;
10219
10529
  }
10220
10530
  }
10221
10531
  await Promise.race([job.closedPromise, new Promise((res) => setTimeout(res, 5e3))]);
10532
+ if (job.running) {
10533
+ job.running = false;
10534
+ job.signalClosed();
10535
+ }
10222
10536
  }
10223
10537
  return snapshot(job);
10224
10538
  }
@@ -10252,6 +10566,12 @@ ${job.output.slice(start)}`;
10252
10566
  }
10253
10567
  const remaining = Math.max(800, deadlineMs - elapsed());
10254
10568
  await Promise.race([allClose, new Promise((res) => setTimeout(res, remaining))]);
10569
+ for (const job of runningJobs) {
10570
+ if (job.running) {
10571
+ job.running = false;
10572
+ job.signalClosed();
10573
+ }
10574
+ }
10255
10575
  }
10256
10576
  /** Count of still-running jobs — drives the TUI status-bar indicator. */
10257
10577
  runningCount() {
@@ -10282,12 +10602,12 @@ function latestOutputSince(before, after) {
10282
10602
  // src/tools/shell/exec.ts
10283
10603
  import { spawn as spawn4, spawnSync } from "child_process";
10284
10604
  import { existsSync as existsSync8, statSync as statSync5 } from "fs";
10285
- import * as pathMod7 from "path";
10605
+ import * as pathMod8 from "path";
10286
10606
 
10287
10607
  // src/tools/shell-chain.ts
10288
10608
  import { spawn as spawn3 } from "child_process";
10289
10609
  import { closeSync, openSync } from "fs";
10290
- import * as pathMod6 from "path";
10610
+ import * as pathMod7 from "path";
10291
10611
  var UnsupportedSyntaxError = class extends Error {
10292
10612
  constructor(detail) {
10293
10613
  super(`run_command: ${detail}`);
@@ -10554,7 +10874,7 @@ function openRedirects(redirects, cwd) {
10554
10874
  let bothFd = null;
10555
10875
  const toClose = [];
10556
10876
  const open = (target, flags) => {
10557
- const resolved = pathMod6.resolve(cwd, target);
10877
+ const resolved = pathMod7.resolve(cwd, target);
10558
10878
  const fd = openSync(resolved, flags);
10559
10879
  toClose.push(fd);
10560
10880
  return fd;
@@ -11051,16 +11371,16 @@ function resolveExecutable(cmd, opts = {}) {
11051
11371
  const platform = opts.platform ?? process.platform;
11052
11372
  if (platform !== "win32") return cmd;
11053
11373
  if (!cmd) return cmd;
11054
- if (cmd.includes("/") || cmd.includes("\\") || pathMod7.isAbsolute(cmd)) return cmd;
11055
- if (pathMod7.extname(cmd)) return cmd;
11374
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod8.isAbsolute(cmd)) return cmd;
11375
+ if (pathMod8.extname(cmd)) return cmd;
11056
11376
  const env = opts.env ?? process.env;
11057
11377
  const pathExt = (getEnvCaseInsensitive(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
11058
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod7.delimiter);
11378
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod8.delimiter);
11059
11379
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
11060
11380
  const isFile = opts.isFile ?? defaultIsFile;
11061
11381
  for (const dir of pathDirs) {
11062
11382
  for (const ext of pathExt) {
11063
- const full = pathMod7.win32.join(dir, cmd + ext);
11383
+ const full = pathMod8.win32.join(dir, cmd + ext);
11064
11384
  if (isFile(full)) return full;
11065
11385
  }
11066
11386
  }
@@ -11176,8 +11496,8 @@ function withUtf8Codepage(cmdline) {
11176
11496
  function isBareWindowsName(s) {
11177
11497
  if (!s) return false;
11178
11498
  if (s.includes("/") || s.includes("\\")) return false;
11179
- if (pathMod7.isAbsolute(s)) return false;
11180
- if (pathMod7.extname(s)) return false;
11499
+ if (pathMod8.isAbsolute(s)) return false;
11500
+ if (pathMod8.extname(s)) return false;
11181
11501
  return true;
11182
11502
  }
11183
11503
  function quoteForCmdExe(arg) {
@@ -11198,7 +11518,7 @@ var NeedsConfirmationError = class extends Error {
11198
11518
  }
11199
11519
  };
11200
11520
  function registerShellTools(registry, opts) {
11201
- const rootDir = pathMod8.resolve(opts.rootDir);
11521
+ const rootDir = pathMod9.resolve(opts.rootDir);
11202
11522
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
11203
11523
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
11204
11524
  const jobs = opts.jobs ?? new JobRegistry();
@@ -11264,7 +11584,7 @@ function registerShellTools(registry, opts) {
11264
11584
  });
11265
11585
  registry.register({
11266
11586
  name: "run_background",
11267
- description: "Spawn a long-running process (dev server, watcher) 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`, kill with `stop_job`, list with `list_jobs`.\n\nSingle process only \u2014 chains / redirects / `cd` work as in run_command, but a typical dev-server invocation is one binary. Use the binary's own --cwd / --prefix flag for subdirectories. Vite gotcha: npm's `--prefix` only finds package.json; vite's server root still uses process cwd \u2014 pass `vite <project-dir>` instead.\n\nUSE THIS \u2014 not run_command \u2014 for: npm/yarn/pnpm dev, uvicorn / flask run, cargo watch, tsc --watch, webpack serve, anything with dev/serve/watch in the name.",
11587
+ 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 chains / redirects / `cd` work as in run_command, but a typical invocation is one binary. Use the binary's own --cwd / --prefix flag for subdirectories. Vite gotcha: npm's `--prefix` only finds package.json; vite's server root still uses process cwd \u2014 pass `vite <project-dir>` instead.\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.",
11268
11588
  parameters: {
11269
11589
  type: "object",
11270
11590
  properties: {
@@ -11337,7 +11657,7 @@ function registerShellTools(registry, opts) {
11337
11657
  });
11338
11658
  registry.register({
11339
11659
  name: "wait_for_job",
11340
- description: "Block until a background job exits or produces new output, bounded by `timeoutMs`. Use this instead of polling `job_output` with identical args when you're intentionally waiting for state to change. Returns JSON with `exited`, `exitCode`, and `latestOutput`.",
11660
+ description: "Block server-side until a background job finishes (or, opt-in, until it produces new output), bounded by `timeoutMs`. Costs ONE tool call regardless of how long the wait runs \u2014 use this instead of polling `job_output` in a loop. Returns JSON with `exited`, `exitCode`, and `latestOutput`.\n\n`waitFor` controls the wake condition:\n- `'exit'` (default) \u2014 only wake on the job exiting (or the timeout). Right for downloads, installs, builds, anything one-shot. Chatty progress bars do NOT wake the wait.\n- `'output-or-exit'` \u2014 also wake whenever the job writes a new line. Right for tailing a dev server / watcher and reacting to a specific log line.\n\nFor a download or install, set `timeoutMs` to the slowest reasonable end-to-end (e.g. 300_000 for a 5-min wheel install).",
11341
11661
  readOnly: true,
11342
11662
  parallelSafe: true,
11343
11663
  stormExempt: true,
@@ -11347,13 +11667,21 @@ function registerShellTools(registry, opts) {
11347
11667
  jobId: { type: "integer", description: "Job id returned by run_background." },
11348
11668
  timeoutMs: {
11349
11669
  type: "integer",
11350
- description: "Max time to block before returning if nothing changes. Clamped to 0..30000. Default 5000."
11670
+ description: "Max time to block before returning if the wake condition hasn't fired. Clamped to 0..300000. Default 5000."
11671
+ },
11672
+ waitFor: {
11673
+ type: "string",
11674
+ enum: ["exit", "output-or-exit"],
11675
+ description: "Wake condition. 'exit' = only on job exit (right for downloads / installs / builds). 'output-or-exit' = also on any new output (right for tailing a dev server). Default 'exit'."
11351
11676
  }
11352
11677
  },
11353
11678
  required: ["jobId"]
11354
11679
  },
11355
11680
  fn: async (args) => {
11356
- const out = await jobs.waitForJob(args.jobId, { timeoutMs: args.timeoutMs });
11681
+ const out = await jobs.waitForJob(args.jobId, {
11682
+ timeoutMs: args.timeoutMs,
11683
+ waitFor: args.waitFor
11684
+ });
11357
11685
  if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
11358
11686
  return {
11359
11687
  jobId: args.jobId,
@@ -12558,8 +12886,12 @@ var McpClient = class {
12558
12886
  }
12559
12887
  });
12560
12888
  promise.catch(() => void 0);
12889
+ const promiseSettled = promise.then(
12890
+ () => void 0,
12891
+ () => void 0
12892
+ );
12561
12893
  try {
12562
- await Promise.race([this.transport.send(frame), promise.then(() => void 0)]);
12894
+ await Promise.race([this.transport.send(frame), promiseSettled]);
12563
12895
  } catch (err) {
12564
12896
  const pending = this.pending.get(id);
12565
12897
  if (pending) clearTimeout(pending.timeout);
@@ -13502,13 +13834,15 @@ Do NOT try to switch via \`run_command\` (\`cd\`, \`pushd\`, etc.) \u2014 your t
13502
13834
  You have TWO tools for running shell commands, and picking the right one is non-negotiable:
13503
13835
 
13504
13836
  - \`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.
13505
- - \`run_background\` \u2014 spawns and detaches after a brief startup window. Use for: **dev servers, watchers, any command 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\`.
13837
+ - \`run_background\` \u2014 spawns and detaches after a brief startup window. Use for:
13838
+ - **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\`.
13839
+ - **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.
13506
13840
 
13507
- **Never use run_command for a dev server.** It will block for 60s, time out, and the user will see a frozen tool call while the server was actually running fine. Always \`run_background\`, then \`job_output\` to peek at the logs when you need to verify something.
13841
+ **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\`.
13508
13842
 
13509
13843
  After \`run_background\`, tools available to you:
13510
13844
  - \`job_output(jobId, tailLines?)\` \u2014 read recent logs to verify startup / debug errors.
13511
- - \`wait_for_job(jobId, timeoutMs?)\` \u2014 block until the job exits or emits new output. Prefer this over repeating identical \`job_output\` calls while you're intentionally waiting.
13845
+ - \`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\`.
13512
13846
  - \`list_jobs\` \u2014 see every job this session (running + exited).
13513
13847
  - \`stop_job(jobId)\` \u2014 SIGTERM \u2192 SIGKILL after grace. Stop before switching port / config.
13514
13848