reasonix 0.40.0 → 0.43.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 (194) hide show
  1. package/README.md +47 -16
  2. package/README.zh-CN.md +19 -13
  3. package/dashboard/app.css +8 -4
  4. package/dashboard/dist/app.js +377 -227
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/dist/cli/acp-DAGPCVFZ.js +713 -0
  7. package/dist/cli/acp-DAGPCVFZ.js.map +1 -0
  8. package/dist/cli/chat-7ES4IBNH.js +50 -0
  9. package/dist/cli/{chunk-E46ECXJD.js → chunk-2425HK6U.js} +2 -1
  10. package/dist/cli/{chunk-E46ECXJD.js.map → chunk-2425HK6U.js.map} +1 -1
  11. package/dist/cli/chunk-25T6CVUP.js +172 -0
  12. package/dist/cli/chunk-25T6CVUP.js.map +1 -0
  13. package/dist/cli/{chunk-7DLHHBGN.js → chunk-2K65GZBT.js} +16 -5
  14. package/dist/cli/chunk-2K65GZBT.js.map +1 -0
  15. package/dist/cli/{chunk-KMWKGPFZ.js → chunk-2KDUS647.js} +14 -4
  16. package/dist/cli/chunk-2KDUS647.js.map +1 -0
  17. package/dist/cli/chunk-2R4QCDOZ.js +11392 -0
  18. package/dist/cli/chunk-2R4QCDOZ.js.map +1 -0
  19. package/dist/cli/{chunk-3Q3C4W66.js → chunk-2UQP6H6T.js} +2 -1
  20. package/dist/cli/{chunk-3Q3C4W66.js.map → chunk-2UQP6H6T.js.map} +1 -1
  21. package/dist/cli/chunk-2Z35JOA4.js +96 -0
  22. package/dist/cli/chunk-2Z35JOA4.js.map +1 -0
  23. package/dist/cli/chunk-32TIKD5U.js +54 -0
  24. package/dist/cli/{chunk-JWCTX5S4.js.map → chunk-32TIKD5U.js.map} +1 -1
  25. package/dist/cli/{chunk-UVRXTSK3.js → chunk-3BXRZFWS.js} +65 -3
  26. package/dist/cli/chunk-3BXRZFWS.js.map +1 -0
  27. package/dist/cli/chunk-3Z6IBU3D.js +249 -0
  28. package/dist/cli/chunk-3Z6IBU3D.js.map +1 -0
  29. package/dist/cli/{chunk-VLNRQMCI.js → chunk-45U62RI3.js} +12 -5
  30. package/dist/cli/chunk-45U62RI3.js.map +1 -0
  31. package/dist/cli/{chunk-5GKJLNP2.js → chunk-4QUNBQQ2.js} +3 -2
  32. package/dist/cli/{chunk-5GKJLNP2.js.map → chunk-4QUNBQQ2.js.map} +1 -1
  33. package/dist/cli/{chunk-R4YTW7PR.js → chunk-5JJRUIPA.js} +57 -12
  34. package/dist/cli/chunk-5JJRUIPA.js.map +1 -0
  35. package/dist/cli/{chunk-HCC42PEI.js → chunk-6AK4EY3D.js} +12 -6
  36. package/dist/cli/chunk-6AK4EY3D.js.map +1 -0
  37. package/dist/cli/chunk-6G3CUUFG.js +34320 -0
  38. package/dist/cli/chunk-6G3CUUFG.js.map +1 -0
  39. package/dist/cli/{chunk-XST7BSZJ.js → chunk-6PBZN4VI.js} +21 -3
  40. package/dist/cli/chunk-6PBZN4VI.js.map +1 -0
  41. package/dist/cli/{chunk-A5LSGEEK.js → chunk-6PZ3CXBP.js} +88 -66
  42. package/dist/cli/chunk-6PZ3CXBP.js.map +1 -0
  43. package/dist/cli/chunk-74EX7SUH.js +25293 -0
  44. package/dist/cli/chunk-74EX7SUH.js.map +1 -0
  45. package/dist/cli/{chunk-FFNOMR32.js → chunk-7O5ALB4C.js} +3 -2
  46. package/dist/cli/{chunk-FFNOMR32.js.map → chunk-7O5ALB4C.js.map} +1 -1
  47. package/dist/cli/{chunk-UCMTWZKU.js → chunk-DOYHN4KB.js} +3 -2
  48. package/dist/cli/{chunk-UCMTWZKU.js.map → chunk-DOYHN4KB.js.map} +1 -1
  49. package/dist/cli/{chunk-XJLZ4HKU.js → chunk-F3PXYSNN.js} +3 -2
  50. package/dist/cli/{chunk-XJLZ4HKU.js.map → chunk-F3PXYSNN.js.map} +1 -1
  51. package/dist/cli/{chunk-XHQIK7B6.js → chunk-FHOGSSCH.js} +4 -3
  52. package/dist/cli/{chunk-XHQIK7B6.js.map → chunk-FHOGSSCH.js.map} +1 -1
  53. package/dist/cli/{chunk-IYF36OCJ.js → chunk-H6PS7IUE.js} +3 -2
  54. package/dist/cli/{chunk-IYF36OCJ.js.map → chunk-H6PS7IUE.js.map} +1 -1
  55. package/dist/cli/{chunk-ZTLZO42A.js → chunk-HFEAY5DT.js} +3 -2
  56. package/dist/cli/{chunk-ZTLZO42A.js.map → chunk-HFEAY5DT.js.map} +1 -1
  57. package/dist/cli/{chunk-FWGEHRB7.js → chunk-J5XJHLWM.js} +2 -1
  58. package/dist/cli/{chunk-FWGEHRB7.js.map → chunk-J5XJHLWM.js.map} +1 -1
  59. package/dist/cli/chunk-JMBMLOBP.js +26 -0
  60. package/dist/cli/chunk-JMBMLOBP.js.map +1 -0
  61. package/dist/cli/{chunk-SZH34P45.js → chunk-O52OLQL3.js} +52 -18
  62. package/dist/cli/chunk-O52OLQL3.js.map +1 -0
  63. package/dist/cli/{chunk-4DCHFFEY.js → chunk-OSZC7C6F.js} +3 -2
  64. package/dist/cli/{chunk-4DCHFFEY.js.map → chunk-OSZC7C6F.js.map} +1 -1
  65. package/dist/cli/chunk-P7EKE5ZQ.js +60641 -0
  66. package/dist/cli/chunk-P7EKE5ZQ.js.map +1 -0
  67. package/dist/cli/{chunk-FM57FNPJ.js → chunk-PLHAZOLZ.js} +2 -1
  68. package/dist/cli/{chunk-FM57FNPJ.js.map → chunk-PLHAZOLZ.js.map} +1 -1
  69. package/dist/cli/{chunk-RFX7TYVV.js → chunk-PQXPXJBJ.js} +16 -2
  70. package/dist/cli/chunk-PQXPXJBJ.js.map +1 -0
  71. package/dist/cli/{chunk-DAEAAVDF.js → chunk-PV55UMTO.js} +2 -1
  72. package/dist/cli/{chunk-DAEAAVDF.js.map → chunk-PV55UMTO.js.map} +1 -1
  73. package/dist/cli/{chunk-H7PHYVPM.js → chunk-RE4RAVFF.js} +85 -14
  74. package/dist/cli/chunk-RE4RAVFF.js.map +1 -0
  75. package/dist/cli/chunk-S4XVGLRW.js +499 -0
  76. package/dist/cli/chunk-S4XVGLRW.js.map +1 -0
  77. package/dist/cli/{chunk-WJ3YX4PZ.js → chunk-SZ5XES2N.js} +3 -2
  78. package/dist/cli/{chunk-WJ3YX4PZ.js.map → chunk-SZ5XES2N.js.map} +1 -1
  79. package/dist/cli/{chunk-4X3NY5ZM.js → chunk-TJX6BFZZ.js} +16 -9
  80. package/dist/cli/{chunk-4X3NY5ZM.js.map → chunk-TJX6BFZZ.js.map} +1 -1
  81. package/dist/cli/chunk-TUK7OWJA.js +51 -0
  82. package/dist/cli/{chunk-WKOMCPXP.js → chunk-VK5HG73G.js} +26 -17
  83. package/dist/cli/chunk-VK5HG73G.js.map +1 -0
  84. package/dist/cli/{chunk-CLAN6PVH.js → chunk-XCGGEJTI.js} +21 -8
  85. package/dist/cli/chunk-XCGGEJTI.js.map +1 -0
  86. package/dist/cli/{chunk-SOZE7V7V.js → chunk-XJXDHAES.js} +3 -2
  87. package/dist/cli/{chunk-SOZE7V7V.js.map → chunk-XJXDHAES.js.map} +1 -1
  88. package/dist/cli/chunk-XPDVG52A.js +2648 -0
  89. package/dist/cli/chunk-XPDVG52A.js.map +1 -0
  90. package/dist/cli/{chunk-CRPQUBP6.js → chunk-XXC2BYTV.js} +2 -1
  91. package/dist/cli/{chunk-CRPQUBP6.js.map → chunk-XXC2BYTV.js.map} +1 -1
  92. package/dist/cli/{chunk-AVB3WZWU.js → chunk-YFGF5NKA.js} +17 -14
  93. package/dist/cli/{chunk-AVB3WZWU.js.map → chunk-YFGF5NKA.js.map} +1 -1
  94. package/dist/cli/{chunk-ORM6PK57.js → chunk-YQ6NTIIE.js} +2 -1
  95. package/dist/cli/{chunk-ORM6PK57.js.map → chunk-YQ6NTIIE.js.map} +1 -1
  96. package/dist/cli/{chunk-ULBW7DYL.js → chunk-YYQAUTTN.js} +3 -2
  97. package/dist/cli/{chunk-ULBW7DYL.js.map → chunk-YYQAUTTN.js.map} +1 -1
  98. package/dist/cli/chunk-ZZM6QJ4W.js +109 -0
  99. package/dist/cli/chunk-ZZM6QJ4W.js.map +1 -0
  100. package/dist/cli/code-SMKEW6CD.js +154 -0
  101. package/dist/cli/code-SMKEW6CD.js.map +1 -0
  102. package/dist/cli/{commands-FQZOBLLZ.js → commands-FVVB5FZF.js} +7 -5
  103. package/dist/cli/{commands-FQZOBLLZ.js.map → commands-FVVB5FZF.js.map} +1 -1
  104. package/dist/cli/{commit-ZS24SHPG.js → commit-HE4VSPZ7.js} +7 -4
  105. package/dist/cli/{commit-ZS24SHPG.js.map → commit-HE4VSPZ7.js.map} +1 -1
  106. package/dist/cli/{desktop-6OLENOOO.js → desktop-Q7NDXCON.js} +379 -72
  107. package/dist/cli/desktop-Q7NDXCON.js.map +1 -0
  108. package/dist/cli/devtools-YECO25QO.js +3719 -0
  109. package/dist/cli/devtools-YECO25QO.js.map +1 -0
  110. package/dist/cli/diff-435UTPC5.js +165 -0
  111. package/dist/cli/{diff-2VUKNGEI.js.map → diff-435UTPC5.js.map} +1 -1
  112. package/dist/cli/doctor-OT7KH75K.js +27 -0
  113. package/dist/cli/{events-APSVNROZ.js → events-XEFAD5VX.js} +6 -4
  114. package/dist/cli/{events-APSVNROZ.js.map → events-XEFAD5VX.js.map} +1 -1
  115. package/dist/cli/index.js +3233 -123
  116. package/dist/cli/index.js.map +1 -1
  117. package/dist/cli/{mcp-DCKOE5RF.js → mcp-WUL2WO75.js} +6 -4
  118. package/dist/cli/{mcp-DCKOE5RF.js.map → mcp-WUL2WO75.js.map} +1 -1
  119. package/dist/cli/{mcp-browse-D6GBP5RQ.js → mcp-browse-RR7R4XET.js} +34 -19
  120. package/dist/cli/mcp-browse-RR7R4XET.js.map +1 -0
  121. package/dist/cli/{mcp-inspect-KFGFPJ3E.js → mcp-inspect-REGLYBWT.js} +9 -8
  122. package/dist/cli/{mcp-inspect-KFGFPJ3E.js.map → mcp-inspect-REGLYBWT.js.map} +1 -1
  123. package/dist/cli/package.json +3 -0
  124. package/dist/cli/prompt-UW6EFLVR.js +16 -0
  125. package/dist/cli/{prune-sessions-LV33R47N.js → prune-sessions-3RWUBYRS.js} +4 -2
  126. package/dist/cli/{prune-sessions-LV33R47N.js.map → prune-sessions-3RWUBYRS.js.map} +1 -1
  127. package/dist/cli/{replay-WFCYX7XF.js → replay-YOURXV4C.js} +42 -30
  128. package/dist/cli/{replay-WFCYX7XF.js.map → replay-YOURXV4C.js.map} +1 -1
  129. package/dist/cli/{run-IUJYEPMT.js → run-Q6BUXV66.js} +28 -27
  130. package/dist/cli/{run-IUJYEPMT.js.map → run-Q6BUXV66.js.map} +1 -1
  131. package/dist/cli/{server-CN4QPPVJ.js → server-XGDBRWMB.js} +44 -43
  132. package/dist/cli/server-XGDBRWMB.js.map +1 -0
  133. package/dist/cli/{sessions-F5GPGTJN.js → sessions-FH7QVYSY.js} +22 -19
  134. package/dist/cli/{sessions-F5GPGTJN.js.map → sessions-FH7QVYSY.js.map} +1 -1
  135. package/dist/cli/setup-VDS6SVEP.js +618 -0
  136. package/dist/cli/setup-VDS6SVEP.js.map +1 -0
  137. package/dist/cli/stats-MQVI2XQH.js +14 -0
  138. package/dist/cli/update-6ITLPRDV.js +15 -0
  139. package/dist/cli/update-6ITLPRDV.js.map +1 -0
  140. package/dist/cli/version-DAHGZY5N.js +33 -0
  141. package/dist/cli/{version-KQUPV6T5.js.map → version-DAHGZY5N.js.map} +1 -1
  142. package/dist/index.d.ts +157 -103
  143. package/dist/index.js +597 -178
  144. package/dist/index.js.map +1 -1
  145. package/package.json +2 -1
  146. package/dist/cli/chat-G7CUW4ZI.js +0 -45
  147. package/dist/cli/chunk-26UDIXLD.js +0 -16481
  148. package/dist/cli/chunk-26UDIXLD.js.map +0 -1
  149. package/dist/cli/chunk-4YV2GBYG.js +0 -5237
  150. package/dist/cli/chunk-4YV2GBYG.js.map +0 -1
  151. package/dist/cli/chunk-5X7LZJDE.js +0 -36
  152. package/dist/cli/chunk-5X7LZJDE.js.map +0 -1
  153. package/dist/cli/chunk-7DLHHBGN.js.map +0 -1
  154. package/dist/cli/chunk-A5LSGEEK.js.map +0 -1
  155. package/dist/cli/chunk-AFFZF3MW.js +0 -36
  156. package/dist/cli/chunk-AFFZF3MW.js.map +0 -1
  157. package/dist/cli/chunk-CLAN6PVH.js.map +0 -1
  158. package/dist/cli/chunk-CPOV2O73.js +0 -39
  159. package/dist/cli/chunk-CPOV2O73.js.map +0 -1
  160. package/dist/cli/chunk-CPTZ5OHX.js +0 -18
  161. package/dist/cli/chunk-CPTZ5OHX.js.map +0 -1
  162. package/dist/cli/chunk-CZSJILQP.js +0 -854
  163. package/dist/cli/chunk-CZSJILQP.js.map +0 -1
  164. package/dist/cli/chunk-H7PHYVPM.js.map +0 -1
  165. package/dist/cli/chunk-HCC42PEI.js.map +0 -1
  166. package/dist/cli/chunk-JWCTX5S4.js +0 -46
  167. package/dist/cli/chunk-KMWKGPFZ.js.map +0 -1
  168. package/dist/cli/chunk-MRLXEMZ7.js +0 -26
  169. package/dist/cli/chunk-MRLXEMZ7.js.map +0 -1
  170. package/dist/cli/chunk-R4YTW7PR.js.map +0 -1
  171. package/dist/cli/chunk-RFX7TYVV.js.map +0 -1
  172. package/dist/cli/chunk-SZH34P45.js.map +0 -1
  173. package/dist/cli/chunk-UVRXTSK3.js.map +0 -1
  174. package/dist/cli/chunk-VLNRQMCI.js.map +0 -1
  175. package/dist/cli/chunk-WKOMCPXP.js.map +0 -1
  176. package/dist/cli/chunk-XST7BSZJ.js.map +0 -1
  177. package/dist/cli/code-YQGVLIT2.js +0 -147
  178. package/dist/cli/code-YQGVLIT2.js.map +0 -1
  179. package/dist/cli/desktop-6OLENOOO.js.map +0 -1
  180. package/dist/cli/diff-2VUKNGEI.js +0 -153
  181. package/dist/cli/doctor-JO2WNN6C.js +0 -24
  182. package/dist/cli/mcp-browse-D6GBP5RQ.js.map +0 -1
  183. package/dist/cli/prompt-PKCCLLAD.js +0 -13
  184. package/dist/cli/server-CN4QPPVJ.js.map +0 -1
  185. package/dist/cli/setup-WWMDBPSB.js +0 -516
  186. package/dist/cli/setup-WWMDBPSB.js.map +0 -1
  187. package/dist/cli/stats-5RJCATCE.js +0 -12
  188. package/dist/cli/update-GUCWB4UN.js +0 -13
  189. package/dist/cli/version-KQUPV6T5.js +0 -30
  190. /package/dist/cli/{chat-G7CUW4ZI.js.map → chat-7ES4IBNH.js.map} +0 -0
  191. /package/dist/cli/{doctor-JO2WNN6C.js.map → chunk-TUK7OWJA.js.map} +0 -0
  192. /package/dist/cli/{prompt-PKCCLLAD.js.map → doctor-OT7KH75K.js.map} +0 -0
  193. /package/dist/cli/{stats-5RJCATCE.js.map → prompt-UW6EFLVR.js.map} +0 -0
  194. /package/dist/cli/{update-GUCWB4UN.js.map → stats-MQVI2XQH.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.",
@@ -927,6 +966,7 @@ var EN = {
927
966
  applied: "applied",
928
967
  rejected: "rejected",
929
968
  noDashboard: "Suppress the auto-launched embedded web dashboard.",
969
+ openDashboardHint: "Open the dashboard URL in your default browser as soon as the server is ready. No-op when --no-dashboard is set.",
930
970
  dashboardPortHint: "Pin the dashboard to a fixed port (1\u201365535). Stable across restarts \u2014 required for SSH tunnels. Default: ephemeral.",
931
971
  dashboardPortInvalid: "\u25B2 ignoring --dashboard-port={value} (must be an integer 1\u201365535) \u2014 falling back to ephemeral",
932
972
  dashboardAutoStartFailed: "\u25B2 dashboard auto-start failed ({reason}) \u2014 try /dashboard, or pass --no-dashboard to silence",
@@ -1091,7 +1131,13 @@ var EN = {
1091
1131
  jsonHintCatalog: "output as JSON",
1092
1132
  jsonHintReport: "output the inspection report as JSON",
1093
1133
  modelOverrideFlash: "override the model (default: deepseek-v4-flash)",
1094
- skipConfirmHint: "skip the confirmation prompt"
1134
+ skipConfirmHint: "skip the confirmation prompt",
1135
+ yoloHint: "auto-approve plan checkpoints for this invocation (equivalent to editMode=yolo without mutating config)"
1136
+ },
1137
+ code: {
1138
+ workspaceConflict: "\u26A0 workspace contains another agent platform's files ({platforms}). Reasonix Code may read them as project content; relaunch with --dir <your-project> if that's not what you want.\n",
1139
+ systemAppendEmpty: "--system-append is empty \u2014 no prompt text will be appended\n",
1140
+ systemAppendFileReadError: 'Error: cannot read --system-append-file "{filePath}": {errorDetails}\n'
1095
1141
  },
1096
1142
  slash: {
1097
1143
  help: { description: "show the full command reference" },
@@ -1237,6 +1283,14 @@ var EN = {
1237
1283
  logs: {
1238
1284
  description: "tail a background job's output (default last 80 lines)",
1239
1285
  argsHint: "<id> [lines]"
1286
+ },
1287
+ btw: {
1288
+ description: "ask a quick side question \u2014 answered from a blank slate, never added to the conversation context",
1289
+ argsHint: "<question>"
1290
+ },
1291
+ "search-engine": {
1292
+ description: "switch web search backend \u2014 mojeek (default, no deps) or searxng (self-hosted)",
1293
+ argsHint: "<mojeek|searxng> [<endpoint>]"
1240
1294
  }
1241
1295
  },
1242
1296
  wizard: {
@@ -1363,9 +1417,18 @@ var EN = {
1363
1417
  counterDone: "{done}/{total} done ({pct}%) \xB7 {total} steps",
1364
1418
  counterDoneSingular: "{done}/{total} done ({pct}%) \xB7 {total} step"
1365
1419
  },
1420
+ noPlanSummary: "No plan body submitted yet.",
1421
+ detailCollapsedHint: "Ctrl+P expands full plan details.",
1422
+ detailExpandedHint: "Ctrl+P collapses details.",
1423
+ detailHeader: "Plan details",
1424
+ detailWindow: "showing lines {start}-{end} of {total}",
1425
+ detailScrollHint: "PgUp/PgDn scroll details \xB7 Home/End jump",
1366
1426
  reviseTitle: "Revise plan",
1367
1427
  reviseSteps: "{count} steps",
1368
- reviseFooter: "\u2191\u2193 focus \xB7 space toggle skip \xB7 k/j move \xB7 \u23CE accept \xB7 esc cancel"
1428
+ reviseFooter: "\u2191\u2193 focus \xB7 space toggle skip \xB7 k/j move \xB7 \u23CE accept \xB7 esc cancel",
1429
+ riskMed: " med",
1430
+ riskHigh: " high",
1431
+ completeMsg: "\u25B8 plan complete \u2014 all {total} step{s} done \xB7 archived"
1369
1432
  },
1370
1433
  app: {
1371
1434
  walkCancelledRemaining: "\u25B8 walk cancelled \u2014 {count} block(s) still pending.",
@@ -1385,6 +1448,9 @@ var EN = {
1385
1448
  notedVerbAppended: "appended to",
1386
1449
  memoryWriteFailed: "# memory write failed",
1387
1450
  commandFailed: "! command failed",
1451
+ btwUsage: "\u25B8 /btw <question> \u2014 ask a side question without polluting the conversation context.",
1452
+ btwHeader: "\u226B btw",
1453
+ btwFailed: "/btw failed",
1388
1454
  restoreCodeOnly: "\u25B8 /restore is code-mode only",
1389
1455
  hookUserPromptSubmit: "UserPromptSubmit hook",
1390
1456
  hookStop: "Stop hook",
@@ -1462,6 +1528,7 @@ var EN = {
1462
1528
  basic: {
1463
1529
  newInfo: "\u25B8 new conversation \u2014 dropped {count} message(s) from context. Same session, fresh slate.",
1464
1530
  newInfoArchived: '\u25B8 new conversation \u2014 dropped {count} message(s) from context. Prior transcript archived as "{archived}" (visible under Sessions).',
1531
+ newInfoSystemReloaded: " \xB7 REASONIX.md / project memory reloaded (next turn pays one cache miss)",
1465
1532
  helpTitle: "Commands:",
1466
1533
  helpShellTitle: "Shell shortcut:",
1467
1534
  helpShell: " !<cmd> run <cmd> in the sandbox root; output goes into",
@@ -1806,7 +1873,8 @@ var EN = {
1806
1873
  mb: " MB",
1807
1874
  evt: " evt",
1808
1875
  editsLabel: "edits:",
1809
- mcpLoading: "MCP"
1876
+ mcpLoading: "MCP",
1877
+ ctx: "ctx"
1810
1878
  },
1811
1879
  editMode: {
1812
1880
  plan: "PLAN MODE",
@@ -2113,7 +2181,8 @@ var EN = {
2113
2181
  healthy: "healthy \xB7 {ms}ms",
2114
2182
  slow: "slow \xB7 {ms}ms",
2115
2183
  verySlow: "very slow \xB7 {ms}ms",
2116
- slowToast: "\u26A0 MCP `{name}` slow \xB7 {seconds}s p95 over the last {sampleSize} calls"
2184
+ slowToast: "\u26A0 MCP `{name}` slow \xB7 {seconds}s p95 over the last {sampleSize} calls",
2185
+ emptyHint: "\u2139 no MCP servers configured \u2014 try: `reasonix setup` to re-pick, or `reasonix mcp install filesystem`"
2117
2186
  },
2118
2187
  denyContextInput: {
2119
2188
  description: "Tell the agent why you denied this. The next attempt will see your reason as additional context."
@@ -2178,7 +2247,9 @@ var EN = {
2178
2247
  reconnect: "reconnect\u2026",
2179
2248
  initDetail: "initialise \u2192 tools/list \u2192 resources/list",
2180
2249
  reconnectDetail: "tearing down \xB7 re-handshake \xB7 listing tools",
2181
- disabledDetail: "via /mcp disable {name}"
2250
+ disabledDetail: "via /mcp disable {name}",
2251
+ failedSetupHint: "\u2192 run `reasonix setup` to remove this entry, or fix the underlying issue (missing npm package, network, etc.).",
2252
+ failedSetupConfigHint: "\u2192 run `reasonix setup` to remove broken entries from your saved config."
2182
2253
  },
2183
2254
  checkpointPicker: {
2184
2255
  title: "restore a checkpoint \u2014 {workspace}",
@@ -2237,7 +2308,10 @@ var zhCN = {
2237
2308
  cancel: "\u53D6\u6D88",
2238
2309
  confirm: "\u786E\u8BA4",
2239
2310
  back: "\u8FD4\u56DE",
2240
- next: "\u4E0B\u4E00\u6B65"
2311
+ next: "\u4E0B\u4E00\u6B65",
2312
+ tool: "\u5DE5\u5177",
2313
+ running: "\u8FD0\u884C\u4E2D",
2314
+ noTurns: "(\u6682\u65E0\u5BF9\u8BDD)"
2241
2315
  },
2242
2316
  cli: {
2243
2317
  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",
@@ -2273,6 +2347,7 @@ var zhCN = {
2273
2347
  applied: "\u5DF2\u5E94\u7528",
2274
2348
  rejected: "\u5DF2\u62D2\u7EDD",
2275
2349
  noDashboard: "\u7981\u6B62\u81EA\u52A8\u542F\u52A8\u5D4C\u5165\u5F0F Web \u4EEA\u8868\u677F\u3002",
2350
+ openDashboardHint: "\u670D\u52A1\u5C31\u7EEA\u540E\u7ACB\u5373\u5728\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u4EEA\u8868\u677F\u5730\u5740\u3002\u8BBE\u7F6E\u4E86 --no-dashboard \u65F6\u4E0D\u751F\u6548\u3002",
2276
2351
  dashboardPortHint: "\u5C06\u4EEA\u8868\u677F\u7ED1\u5B9A\u5230\u56FA\u5B9A\u7AEF\u53E3 (1\u201365535)\u3002\u91CD\u542F\u540E\u4FDD\u6301\u7A33\u5B9A \u2014 SSH \u96A7\u9053\u8BBF\u95EE\u5FC5\u9700\u3002\u9ED8\u8BA4\u4E3A\u4E34\u65F6\u7AEF\u53E3\u3002",
2277
2352
  dashboardPortInvalid: "\u25B2 \u5FFD\u7565 --dashboard-port={value} (\u5FC5\u987B\u4E3A 1\u201365535 \u4E4B\u95F4\u7684\u6574\u6570) \u2014 \u56DE\u9000\u5230\u4E34\u65F6\u7AEF\u53E3",
2278
2353
  dashboardAutoStartFailed: "\u25B2 \u4EEA\u8868\u677F\u81EA\u52A8\u542F\u52A8\u5931\u8D25 ({reason}) \u2014 \u5C1D\u8BD5 /dashboard\uFF0C\u6216\u4F20\u9012 --no-dashboard \u4EE5\u9759\u9ED8",
@@ -2434,7 +2509,13 @@ var zhCN = {
2434
2509
  jsonHintCatalog: "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA",
2435
2510
  jsonHintReport: "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA\u68C0\u67E5\u62A5\u544A",
2436
2511
  modelOverrideFlash: "\u8986\u76D6\u6A21\u578B\uFF08\u9ED8\u8BA4\uFF1Adeepseek-v4-flash\uFF09",
2437
- skipConfirmHint: "\u8DF3\u8FC7\u786E\u8BA4\u63D0\u793A"
2512
+ skipConfirmHint: "\u8DF3\u8FC7\u786E\u8BA4\u63D0\u793A",
2513
+ yoloHint: "\u81EA\u52A8\u6279\u51C6\u672C\u6B21\u8C03\u7528\u7684\u8BA1\u5212\u68C0\u67E5\u70B9\uFF08\u7B49\u540C\u4E8E editMode=yolo\uFF0C\u4F46\u4E0D\u4FEE\u6539\u914D\u7F6E\u6587\u4EF6\uFF09"
2514
+ },
2515
+ code: {
2516
+ workspaceConflict: "\u26A0 \u5DE5\u4F5C\u533A\u5305\u542B\u53E6\u4E00\u4E2A\u667A\u80FD\u4F53\u5E73\u53F0\u7684\u6587\u4EF6 ({platforms})\u3002Reasonix Code \u53EF\u80FD\u4F1A\u5C06\u5176\u4F5C\u4E3A\u9879\u76EE\u5185\u5BB9\u8BFB\u53D6\uFF1B\u5982\u679C\u4E0D\u662F\u60A8\u60F3\u8981\u7684\uFF0C\u8BF7\u4F7F\u7528 --dir <your-project> \u91CD\u65B0\u542F\u52A8\u3002\n",
2517
+ systemAppendEmpty: "--system-append \u4E3A\u7A7A \u2014 \u4E0D\u4F1A\u8FFD\u52A0\u4EFB\u4F55\u63D0\u793A\u6587\u672C\n",
2518
+ systemAppendFileReadError: '\u9519\u8BEF\uFF1A\u65E0\u6CD5\u8BFB\u53D6 --system-append-file "{filePath}"\uFF1A{errorDetails}\n'
2438
2519
  },
2439
2520
  slash: {
2440
2521
  help: { description: "\u663E\u793A\u5B8C\u6574\u547D\u4EE4\u53C2\u8003" },
@@ -2584,6 +2665,14 @@ var zhCN = {
2584
2665
  logs: {
2585
2666
  description: "\u8DDF\u8E2A\u540E\u53F0\u4F5C\u4E1A\u7684\u8F93\u51FA\uFF08\u9ED8\u8BA4\u6700\u540E 80 \u884C\uFF09",
2586
2667
  argsHint: "<id> [lines]"
2668
+ },
2669
+ btw: {
2670
+ description: "\u987A\u4FBF\u95EE\u4E00\u4E0B \u2014 \u4ECE\u7A7A\u767D\u4E0A\u4E0B\u6587\u56DE\u7B54\uFF0C\u4E0D\u5199\u5165\u4F1A\u8BDD\u5386\u53F2",
2671
+ argsHint: "<question>"
2672
+ },
2673
+ "search-engine": {
2674
+ description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u4F9D\u8D56\uFF09\u6216 searxng\uFF08\u81EA\u6258\u7BA1\uFF09",
2675
+ argsHint: "<mojeek|searxng> [<endpoint>]"
2587
2676
  }
2588
2677
  },
2589
2678
  wizard: {
@@ -2710,9 +2799,18 @@ var zhCN = {
2710
2799
  counterDone: "{done}/{total} \u5DF2\u5B8C\u6210\uFF08{pct}%\uFF09 \xB7 \u5171 {total} \u6B65",
2711
2800
  counterDoneSingular: "{done}/{total} \u5DF2\u5B8C\u6210\uFF08{pct}%\uFF09 \xB7 \u5171 {total} \u6B65"
2712
2801
  },
2802
+ noPlanSummary: "\u5C1A\u672A\u63D0\u4EA4\u8BA1\u5212\u5185\u5BB9\u3002",
2803
+ detailCollapsedHint: "Ctrl+P \u5C55\u5F00\u5B8C\u6574\u8BA1\u5212\u8BE6\u60C5\u3002",
2804
+ detailExpandedHint: "Ctrl+P \u6536\u8D77\u8BE6\u60C5\u3002",
2805
+ detailHeader: "\u8BA1\u5212\u8BE6\u60C5",
2806
+ detailWindow: "\u663E\u793A\u7B2C {start}-{end} \u884C\uFF0C\u5171 {total} \u884C",
2807
+ detailScrollHint: "PgUp/PgDn \u6EDA\u52A8\u8BE6\u60C5 \xB7 Home/End \u8DF3\u8F6C",
2713
2808
  reviseTitle: "\u4FEE\u6539\u8BA1\u5212",
2714
2809
  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"
2810
+ 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",
2811
+ riskMed: " \u4E2D",
2812
+ riskHigh: " \u9AD8",
2813
+ completeMsg: "\u25B8 \u8BA1\u5212\u5B8C\u6210 \u2014 \u5168\u90E8 {total} \u4E2A\u6B65\u9AA4\u5DF2\u5B8C\u6210 \xB7 \u5DF2\u5F52\u6863"
2716
2814
  },
2717
2815
  app: {
2718
2816
  walkCancelledRemaining: "\u25B8 \u6D4F\u89C8\u5DF2\u53D6\u6D88 \u2014 \u8FD8\u6709 {count} \u4E2A\u5F85\u5904\u7406\u7F16\u8F91\u5757\u3002",
@@ -2732,6 +2830,9 @@ var zhCN = {
2732
2830
  notedVerbAppended: "\u8FFD\u52A0\u5230",
2733
2831
  memoryWriteFailed: "# \u8BB0\u5FC6\u5199\u5165\u5931\u8D25",
2734
2832
  commandFailed: "! \u547D\u4EE4\u5931\u8D25",
2833
+ 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",
2834
+ btwHeader: "\u226B btw",
2835
+ btwFailed: "/btw \u8C03\u7528\u5931\u8D25",
2735
2836
  restoreCodeOnly: "\u25B8 /restore \u4EC5\u5728\u4EE3\u7801\u6A21\u5F0F\u53EF\u7528",
2736
2837
  hookUserPromptSubmit: "UserPromptSubmit \u94A9\u5B50",
2737
2838
  hookStop: "Stop \u94A9\u5B50",
@@ -2809,6 +2910,7 @@ var zhCN = {
2809
2910
  basic: {
2810
2911
  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
2912
  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",
2913
+ newInfoSystemReloaded: " \xB7 REASONIX.md / \u9879\u76EE\u8BB0\u5FC6\u5DF2\u91CD\u65B0\u52A0\u8F7D\uFF08\u4E0B\u4E00\u8F6E\u4E00\u6B21\u6027 cache miss\uFF09",
2812
2914
  helpTitle: "\u547D\u4EE4\uFF1A",
2813
2915
  helpShellTitle: "Shell \u5FEB\u6377\u65B9\u5F0F\uFF1A",
2814
2916
  helpShell: " !<cmd> \u5728\u6C99\u7BB1\u6839\u76EE\u5F55\u8FD0\u884C <cmd>\uFF1B\u8F93\u51FA\u8FDB\u5165\u5BF9\u8BDD",
@@ -3153,7 +3255,8 @@ var zhCN = {
3153
3255
  mb: " MB",
3154
3256
  evt: " \u4E8B\u4EF6",
3155
3257
  editsLabel: "\u7F16\u8F91:",
3156
- mcpLoading: "MCP"
3258
+ mcpLoading: "MCP",
3259
+ ctx: "\u4E0A\u4E0B\u6587"
3157
3260
  },
3158
3261
  editMode: {
3159
3262
  plan: "\u8BA1\u5212",
@@ -3460,7 +3563,8 @@ var zhCN = {
3460
3563
  healthy: "\u6B63\u5E38 \xB7 {ms}ms",
3461
3564
  slow: "\u7F13\u6162 \xB7 {ms}ms",
3462
3565
  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"
3566
+ slowToast: "\u26A0 MCP `{name}` \u54CD\u5E94\u7F13\u6162 \xB7 P95 {seconds}s \xB7 \u6700\u8FD1 {sampleSize} \u6B21\u8C03\u7528",
3567
+ 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
3568
  },
3465
3569
  denyContextInput: {
3466
3570
  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"
@@ -3525,7 +3629,9 @@ var zhCN = {
3525
3629
  reconnect: "\u91CD\u8FDE\u4E2D\u2026",
3526
3630
  initDetail: "\u521D\u59CB\u5316 \u2192 tools/list \u2192 resources/list",
3527
3631
  reconnectDetail: "\u65AD\u5F00\u65E7\u8FDE\u63A5 \xB7 \u91CD\u65B0\u63E1\u624B \xB7 \u5217\u51FA\u5DE5\u5177",
3528
- disabledDetail: "\u901A\u8FC7 /mcp disable {name}"
3632
+ disabledDetail: "\u901A\u8FC7 /mcp disable {name}",
3633
+ 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",
3634
+ failedSetupConfigHint: "\u2192 \u8FD0\u884C `reasonix setup` \u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u4E2D\u79FB\u9664\u635F\u574F\u7684\u6761\u76EE\u3002"
3529
3635
  },
3530
3636
  checkpointPicker: {
3531
3637
  title: "\u6062\u590D\u68C0\u67E5\u70B9 \u2014 {workspace}",
@@ -4509,7 +4615,7 @@ import {
4509
4615
  writeFileSync as writeFileSync2
4510
4616
  } from "fs";
4511
4617
  import { homedir as homedir3 } from "os";
4512
- import { dirname as dirname3, join as join4 } from "path";
4618
+ import { dirname as dirname3, join as join4, posix as posixPath, win32 as win32Path } from "path";
4513
4619
  function sessionsDir() {
4514
4620
  return join4(homedir3(), ".reasonix", "sessions");
4515
4621
  }
@@ -4796,6 +4902,23 @@ var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
4796
4902
  var FORCE_SUMMARY_THRESHOLD = 0.8;
4797
4903
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
4798
4904
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
4905
+ var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
4906
+ var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
4907
+ function extractPinnedSkills(head) {
4908
+ const pinned = /* @__PURE__ */ new Map();
4909
+ const stubbedHead = head.map((msg) => {
4910
+ if (typeof msg.content !== "string") return msg;
4911
+ let hit = false;
4912
+ const next = msg.content.replace(SKILL_PIN_REGEX, (full, name) => {
4913
+ pinned.delete(name);
4914
+ pinned.set(name, full);
4915
+ hit = true;
4916
+ return `[skill ${JSON.stringify(name)} memo \u2014 preserved separately, do not summarize.]`;
4917
+ });
4918
+ return hit ? { ...msg, content: next } : msg;
4919
+ });
4920
+ return { stubbedHead, pinnedBodies: [...pinned.values()] };
4921
+ }
4799
4922
  var ContextManager = class {
4800
4923
  constructor(deps) {
4801
4924
  this.deps = deps;
@@ -4865,11 +4988,17 @@ var ContextManager = class {
4865
4988
  const tail = all.slice(boundary);
4866
4989
  const headTokens = totalTokens - cumTokens;
4867
4990
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
4868
- const summary = await this.summarizeForFold(head);
4991
+ const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
4992
+ const summary = await this.summarizeForFold(stubbedHead);
4869
4993
  if (!summary) return noop;
4994
+ const memoTail = pinnedBodies.length > 0 ? `
4995
+
4996
+ ${SKILL_PIN_MEMO_HEADER}
4997
+
4998
+ ${pinnedBodies.join("\n\n")}` : "";
4870
4999
  const summaryMsg = {
4871
5000
  role: "assistant",
4872
- content: HISTORY_FOLD_MARKER + summary
5001
+ content: HISTORY_FOLD_MARKER + summary + memoTail
4873
5002
  };
4874
5003
  const replacement = [summaryMsg, ...tail];
4875
5004
  this.deps.log.compactInPlace(replacement);
@@ -5094,6 +5223,8 @@ function buildSyntheticAssistantMessage(content, fallbackModel) {
5094
5223
  }
5095
5224
 
5096
5225
  // src/loop/force-summary.ts
5226
+ var PAUSE_SUMMARY_MODEL = "deepseek-v4-flash";
5227
+ var PAUSE_SUMMARY_EFFORT = "high";
5097
5228
  async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
5098
5229
  try {
5099
5230
  yield { turn: ctx.turn, role: "status", content: t("summary.status") };
@@ -5139,6 +5270,28 @@ ${summary}`;
5139
5270
  yield { turn: ctx.turn, role: "done", content: "" };
5140
5271
  }
5141
5272
  }
5273
+ async function summarizePartialProgress(ctx) {
5274
+ try {
5275
+ const messages = ctx.buildMessages();
5276
+ messages.push({
5277
+ role: "user",
5278
+ content: "You're being paused at a checkpoint, not stopped. In 3-6 sentences of plain prose, tell the parent agent: (1) what you accomplished so far, (2) what's still left, (3) any blockers or open questions. Be concrete \u2014 mention specific files / functions / tool results \u2014 so the parent can decide whether to resume you or take over. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
5279
+ });
5280
+ const resp = await ctx.client.chat({
5281
+ model: PAUSE_SUMMARY_MODEL,
5282
+ messages,
5283
+ signal: ctx.signal,
5284
+ thinking: thinkingModeForModel(PAUSE_SUMMARY_MODEL),
5285
+ reasoningEffort: PAUSE_SUMMARY_EFFORT
5286
+ });
5287
+ const cleaned = stripHallucinatedToolMarkup(resp.content?.trim() ?? "");
5288
+ if (!cleaned) return null;
5289
+ const stats = ctx.recordStats(PAUSE_SUMMARY_MODEL, resp.usage ?? new Usage());
5290
+ return { summary: cleaned, stats };
5291
+ } catch {
5292
+ return null;
5293
+ }
5294
+ }
5142
5295
 
5143
5296
  // src/loop/shrink.ts
5144
5297
  function looksLikeCompleteJson(s) {
@@ -5184,6 +5337,10 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
5184
5337
  }
5185
5338
 
5186
5339
  // src/loop/healing.ts
5340
+ var _stampSeq = 0;
5341
+ function stampMissingIds(calls) {
5342
+ return calls.map((c) => c.id ? c : { ...c, id: `z-ext-${Date.now()}-${_stampSeq++}` });
5343
+ }
5187
5344
  function fixToolCallPairing(messages) {
5188
5345
  const out = [];
5189
5346
  let droppedAssistantCalls = 0;
@@ -5191,9 +5348,10 @@ function fixToolCallPairing(messages) {
5191
5348
  for (let i = 0; i < messages.length; i++) {
5192
5349
  const msg = messages[i];
5193
5350
  if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
5351
+ const calls = stampMissingIds(msg.tool_calls);
5194
5352
  const needed = /* @__PURE__ */ new Set();
5195
- for (const call of msg.tool_calls) {
5196
- if (call?.id) needed.add(call.id);
5353
+ for (const call of calls) {
5354
+ if (call.id) needed.add(call.id);
5197
5355
  }
5198
5356
  const candidates = [];
5199
5357
  let j = i + 1;
@@ -5207,7 +5365,7 @@ function fixToolCallPairing(messages) {
5207
5365
  j++;
5208
5366
  }
5209
5367
  if (needed.size === 0) {
5210
- out.push(msg);
5368
+ out.push({ ...msg, tool_calls: calls });
5211
5369
  for (const r of candidates) out.push(r);
5212
5370
  i = j - 1;
5213
5371
  } else {
@@ -5310,17 +5468,25 @@ var TurnFailureTracker = class {
5310
5468
  // src/memory/runtime.ts
5311
5469
  import { createHash } from "crypto";
5312
5470
  var ImmutablePrefix = class {
5471
+ /** Stable across turns; rebuilt only on /new when REASONIX.md changed on disk. */
5313
5472
  system;
5314
5473
  /** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
5315
5474
  _toolSpecs;
5316
5475
  fewShots;
5317
- /** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
5476
+ /** Invalidated by addTool / removeTool / replaceSystem; bypassing any of those leaves cache stale → fingerprint diverges from sent prefix. */
5318
5477
  _fingerprintCache = null;
5319
5478
  constructor(opts) {
5320
5479
  this.system = opts.system;
5321
5480
  this._toolSpecs = [...opts.toolSpecs ?? []];
5322
5481
  this.fewShots = Object.freeze([...opts.fewShots ?? []]);
5323
5482
  }
5483
+ /** Replaces the system prompt; returns true iff the string actually changed. Caller must accept a cache miss on the next turn. */
5484
+ replaceSystem(s) {
5485
+ if (this.system === s) return false;
5486
+ this.system = s;
5487
+ this._fingerprintCache = null;
5488
+ return true;
5489
+ }
5324
5490
  get toolSpecs() {
5325
5491
  return this._toolSpecs;
5326
5492
  }
@@ -5745,12 +5911,14 @@ var CacheFirstLoop = class {
5745
5911
  /** One-shot 80% warning latch — cleared by setBudget so a bump re-arms at the new boundary. */
5746
5912
  _budgetWarned = false;
5747
5913
  sessionName;
5914
+ onIterBudgetExhausted;
5748
5915
  hooks;
5749
5916
  hookCwd;
5750
5917
  /** PauseGate bridge — defaults to singleton, injectable for tests. */
5751
5918
  confirmationGate;
5752
5919
  /** Number of messages that were pre-loaded from the session file. */
5753
5920
  resumedMessageCount;
5921
+ _rebuildSystem;
5754
5922
  _turn = 0;
5755
5923
  _streamPreference;
5756
5924
  /** Threaded through HTTP + every tool dispatch so Esc cancels in-flight work, not after. */
@@ -5783,33 +5951,15 @@ var CacheFirstLoop = class {
5783
5951
  resolveFailureThreshold(opts.failureThreshold, FAILURE_ESCALATION_THRESHOLD)
5784
5952
  );
5785
5953
  this.maxToolIters = opts.maxToolIters ?? 64;
5954
+ this.onIterBudgetExhausted = opts.onIterBudgetExhausted ?? "summarize";
5786
5955
  this.hooks = opts.hooks ?? [];
5787
5956
  this.hookCwd = opts.hookCwd ?? process.cwd();
5788
5957
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
5958
+ this._rebuildSystem = opts.rebuildSystem ?? null;
5789
5959
  this._streamPreference = opts.stream ?? true;
5790
5960
  this.stream = this._streamPreference;
5791
5961
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
5792
5962
  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
5963
  const isStormExempt = (call) => {
5814
5964
  const name = call.function?.name;
5815
5965
  if (!name) return false;
@@ -5817,7 +5967,7 @@ var CacheFirstLoop = class {
5817
5967
  };
5818
5968
  this.repair = new ToolCallRepair({
5819
5969
  allowedToolNames: allowedNames,
5820
- isMutating,
5970
+ isMutating: (call) => this.isMutating(call),
5821
5971
  isStormExempt,
5822
5972
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
5823
5973
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
@@ -5910,7 +6060,7 @@ var CacheFirstLoop = class {
5910
6060
  }
5911
6061
  }
5912
6062
  }
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. */
6063
+ /** "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
6064
  clearLog() {
5915
6065
  const dropped = this.log.length;
5916
6066
  this.log.compactInPlace([]);
@@ -5924,7 +6074,14 @@ var CacheFirstLoop = class {
5924
6074
  }
5925
6075
  this.scratch.reset();
5926
6076
  this._inflight.clear();
5927
- return { dropped, archived };
6077
+ let systemRebuilt = false;
6078
+ if (this._rebuildSystem) {
6079
+ try {
6080
+ systemRebuilt = this.prefix.replaceSystem(this._rebuildSystem());
6081
+ } catch {
6082
+ }
6083
+ }
6084
+ return { dropped, archived, systemRebuilt };
5928
6085
  }
5929
6086
  configure(opts) {
5930
6087
  if (opts.model !== void 0) this.model = opts.model;
@@ -5970,6 +6127,27 @@ var CacheFirstLoop = class {
5970
6127
  this._escalateThisTurn = true;
5971
6128
  return true;
5972
6129
  }
6130
+ /** A call counts as mutating when its definition reports `readOnly !== true` and any dynamic `readOnlyCheck` doesn't override that for these args. */
6131
+ isMutating(call) {
6132
+ const name = call.function?.name;
6133
+ if (!name) return false;
6134
+ const def = this.tools.get(name);
6135
+ if (!def) return false;
6136
+ if (def.readOnlyCheck) {
6137
+ let args = {};
6138
+ try {
6139
+ args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
6140
+ } catch {
6141
+ }
6142
+ try {
6143
+ if (def.readOnlyCheck(args)) return false;
6144
+ } catch (err) {
6145
+ process.stderr.write(`readOnlyCheck for ${name} threw: ${err.message}
6146
+ `);
6147
+ }
6148
+ }
6149
+ return def.readOnly !== true;
6150
+ }
5973
6151
  async runOneToolCall(call, signal) {
5974
6152
  const name = call.function?.name ?? "";
5975
6153
  const args = call.function?.arguments ?? "{}";
@@ -6207,6 +6385,15 @@ ${reason}`
6207
6385
  thinking: thinkingModeForModel(callModel),
6208
6386
  reasoningEffort: this.reasoningEffort
6209
6387
  })) {
6388
+ if (chunk.reasoningDelta) {
6389
+ reasoningContent += chunk.reasoningDelta;
6390
+ yield {
6391
+ turn: this._turn,
6392
+ role: "assistant_delta",
6393
+ content: "",
6394
+ reasoningDelta: chunk.reasoningDelta
6395
+ };
6396
+ }
6210
6397
  if (chunk.contentDelta) {
6211
6398
  assistantContent += chunk.contentDelta;
6212
6399
  if (bufferForEscalation && !escalationBufFlushed) {
@@ -6231,15 +6418,6 @@ ${reason}`
6231
6418
  };
6232
6419
  }
6233
6420
  }
6234
- if (chunk.reasoningDelta) {
6235
- reasoningContent += chunk.reasoningDelta;
6236
- yield {
6237
- turn: this._turn,
6238
- role: "assistant_delta",
6239
- content: "",
6240
- reasoningDelta: chunk.reasoningDelta
6241
- };
6242
- }
6243
6421
  if (chunk.toolCallDelta) {
6244
6422
  const d = chunk.toolCallDelta;
6245
6423
  const cur = callBuf.get(d.index) ?? {
@@ -6527,6 +6705,19 @@ ${reason}`
6527
6705
  }
6528
6706
  }
6529
6707
  }
6708
+ if (this.onIterBudgetExhausted === "pause") {
6709
+ const partial = await summarizePartialProgress(this.summaryContext());
6710
+ yield {
6711
+ turn: this._turn,
6712
+ role: "paused",
6713
+ content: "",
6714
+ sessionName: this.sessionName ?? void 0,
6715
+ pausedAtIter: this.maxToolIters,
6716
+ partialSummary: partial?.summary
6717
+ };
6718
+ yield { turn: this._turn, role: "done", content: "" };
6719
+ return;
6720
+ }
6530
6721
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
6531
6722
  }
6532
6723
  summaryContext() {
@@ -6844,7 +7035,7 @@ function parseAtQuery(query) {
6844
7035
  trailingSlash: false
6845
7036
  };
6846
7037
  }
6847
- var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
7038
+ var AT_PICKER_PREFIX = /(?:^|\s)@([\p{L}\p{N}_./\\-]*)$/u;
6848
7039
  function detectAtPicker(input) {
6849
7040
  const m = AT_PICKER_PREFIX.exec(input);
6850
7041
  if (!m) return null;
@@ -6930,7 +7121,7 @@ function fuzzySubseqScore(needle, target) {
6930
7121
  const lengthPenalty = Math.floor(target.length / 4);
6931
7122
  return quality + lengthPenalty;
6932
7123
  }
6933
- var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
7124
+ var AT_MENTION_PATTERN = /(?<=^|\s)@([\p{L}\p{N}_./\\-]+)/gu;
6934
7125
  function expandAtMentions(text, rootDir, opts = {}) {
6935
7126
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
6936
7127
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
@@ -7219,6 +7410,12 @@ function parseAllowedTools(raw) {
7219
7410
  const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
7220
7411
  return names.length > 0 ? Object.freeze(names) : void 0;
7221
7412
  }
7413
+ function parseMaxToolIters(raw) {
7414
+ if (raw === void 0) return void 0;
7415
+ const n = Number.parseInt(raw.trim(), 10);
7416
+ if (!Number.isFinite(n) || n < 1) return void 0;
7417
+ return n;
7418
+ }
7222
7419
  var SkillStore = class {
7223
7420
  homeDir;
7224
7421
  projectRoot;
@@ -7349,7 +7546,8 @@ var SkillStore = class {
7349
7546
  path: path2,
7350
7547
  allowedTools: parseAllowedTools(data["allowed-tools"]),
7351
7548
  runAs: parseRunAs(data.runAs),
7352
- model: data.model?.startsWith("deepseek-") ? data.model : void 0
7549
+ model: data.model?.startsWith("deepseek-") ? data.model : void 0,
7550
+ maxToolIters: parseMaxToolIters(data["max-iters"])
7353
7551
  };
7354
7552
  }
7355
7553
  };
@@ -7370,6 +7568,7 @@ Tips:
7370
7568
  - Reference tools by name (run_command, edit_file, search_content, ...)
7371
7569
  - Add \`runAs: subagent\` to frontmatter to spawn an isolated subagent loop
7372
7570
  - Add \`allowed-tools: read_file, search_content\` to scope a subagent's tools
7571
+ - Add \`max-iters: N\` to change the subagent's pause cadence (default 16). This isn't a budget \u2014 the parent resumes on pause, so N is how often the parent gets a checkpoint, not how much total work the subagent gets.
7373
7572
  `;
7374
7573
  }
7375
7574
  function skillIndexLine(s) {
@@ -7620,16 +7819,24 @@ function ensureDir(p) {
7620
7819
  if (!existsSync7(p)) mkdirSync4(p, { recursive: true });
7621
7820
  }
7622
7821
  function formatFrontmatter(e) {
7623
- return [
7822
+ const lines = [
7624
7823
  "---",
7625
7824
  `name: ${e.name}`,
7626
7825
  `description: ${e.description.replace(/\n/g, " ")}`,
7627
7826
  `type: ${e.type}`,
7628
7827
  `scope: ${e.scope}`,
7629
- `created: ${e.createdAt}`,
7630
- "---",
7631
- ""
7632
- ].join("\n");
7828
+ `created: ${e.createdAt}`
7829
+ ];
7830
+ if (e.priority) lines.push(`priority: ${e.priority}`);
7831
+ if (e.expires) lines.push(`expires: ${e.expires}`);
7832
+ lines.push("---", "");
7833
+ return lines.join("\n");
7834
+ }
7835
+ function coercePriority(v) {
7836
+ return v === "low" || v === "medium" || v === "high" ? v : void 0;
7837
+ }
7838
+ function coerceExpires(v) {
7839
+ return v === "project_end" ? v : void 0;
7633
7840
  }
7634
7841
  function todayIso() {
7635
7842
  const d = /* @__PURE__ */ new Date();
@@ -7691,7 +7898,7 @@ var MemoryStore = class {
7691
7898
  }
7692
7899
  const raw = readFileSync9(file, "utf8");
7693
7900
  const { data, body } = parseFrontmatter(raw);
7694
- return {
7901
+ const entry = {
7695
7902
  name: data.name ?? name,
7696
7903
  type: data.type ?? "project",
7697
7904
  scope: data.scope ?? scope,
@@ -7699,6 +7906,11 @@ var MemoryStore = class {
7699
7906
  body: body.trim(),
7700
7907
  createdAt: data.created ?? ""
7701
7908
  };
7909
+ const priority = coercePriority(data.priority);
7910
+ if (priority) entry.priority = priority;
7911
+ const expires = coerceExpires(data.expires);
7912
+ if (expires) entry.expires = expires;
7913
+ return entry;
7702
7914
  }
7703
7915
  /** Skips malformed files — index stays queryable even if one file is hand-edited into nonsense. */
7704
7916
  list() {
@@ -7741,6 +7953,8 @@ var MemoryStore = class {
7741
7953
  body,
7742
7954
  createdAt: todayIso()
7743
7955
  };
7956
+ if (input.priority) entry.priority = input.priority;
7957
+ if (input.expires) entry.expires = input.expires;
7744
7958
  const dir = this.dir(input.scope);
7745
7959
  const file = join8(dir, `${name}.md`);
7746
7960
  const content = `${formatFrontmatter(entry)}${body}
@@ -7824,13 +8038,36 @@ function applyGlobalReasonixMemory(basePrompt, homeDir) {
7824
8038
  "```"
7825
8039
  ].join("\n");
7826
8040
  }
8041
+ function effectivePriority(entry, cfg) {
8042
+ if (entry.priority) return entry.priority;
8043
+ return memoryTypeDefaults(entry.type, cfg).priority;
8044
+ }
8045
+ function highPriorityBlock(entries, cfg) {
8046
+ const high = entries.filter((e) => effectivePriority(e, cfg) === "high");
8047
+ if (high.length === 0) return null;
8048
+ const lines = [
8049
+ "# HIGH PRIORITY constraints (must observe)",
8050
+ "",
8051
+ "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.",
8052
+ ""
8053
+ ];
8054
+ for (const e of high) {
8055
+ const head = `!!! [${e.scope}/${e.type}/${e.name}] ${e.description || "(no description)"}`;
8056
+ lines.push(head);
8057
+ if (e.body) lines.push("", e.body);
8058
+ lines.push("");
8059
+ }
8060
+ return lines.join("\n").trimEnd();
8061
+ }
7827
8062
  function applyUserMemory(basePrompt, opts = {}) {
7828
8063
  if (!memoryEnabled()) return basePrompt;
7829
8064
  const store = new MemoryStore(opts);
7830
8065
  const global = store.loadIndex("global");
7831
8066
  const project = store.hasProjectScope() ? store.loadIndex("project") : null;
7832
- if (!global && !project) return basePrompt;
8067
+ const high = highPriorityBlock(store.list(), opts.cfg);
8068
+ if (!global && !project && !high) return basePrompt;
7833
8069
  const parts = [basePrompt];
8070
+ if (high) parts.push("", high);
7834
8071
  if (global) {
7835
8072
  parts.push(
7836
8073
  "",
@@ -7866,7 +8103,7 @@ function applyMemoryStack(basePrompt, rootDir) {
7866
8103
 
7867
8104
  // src/tools/filesystem.ts
7868
8105
  import { promises as fs4 } from "fs";
7869
- import * as pathMod4 from "path";
8106
+ import * as pathMod5 from "path";
7870
8107
  import picomatch3 from "picomatch";
7871
8108
 
7872
8109
  // src/tools/fs/edit.ts
@@ -8088,15 +8325,149 @@ async function globFiles(ctx, startAbs, args) {
8088
8325
  return lines.join("\n");
8089
8326
  }
8090
8327
 
8328
+ // src/tools/fs/outline.ts
8329
+ import * as pathMod3 from "path";
8330
+ var OUTLINE_MAX_ENTRIES = 30;
8331
+ var OUTLINE_TAIL_KEEP = 5;
8332
+ var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
8333
+ var PY_DECL_RE = /^(?:async\s+)?(def|class)\s+(\w+)/;
8334
+ var GO_DECL_RE = /^(func|type|var|const)\s+(?:\([^)]+\)\s+)?(\w+)/;
8335
+ var RUST_DECL_RE = /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|mod|type|const|static|union)\s+(\w+)/;
8336
+ var RUST_IMPL_RE = /^(?:unsafe\s+)?impl(?:\s*<[^>]+>)?\s+(?:[^{]+\s+for\s+)?(\w+)/;
8337
+ var MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
8338
+ var MD_FENCE_RE = /^```/;
8339
+ var EXT_TO_LANG = {
8340
+ ".ts": "ts",
8341
+ ".tsx": "ts",
8342
+ ".mts": "ts",
8343
+ ".cts": "ts",
8344
+ ".js": "ts",
8345
+ ".jsx": "ts",
8346
+ ".mjs": "ts",
8347
+ ".cjs": "ts",
8348
+ ".py": "py",
8349
+ ".pyi": "py",
8350
+ ".go": "go",
8351
+ ".rs": "rust",
8352
+ ".md": "md",
8353
+ ".markdown": "md",
8354
+ ".mdx": "md"
8355
+ };
8356
+ function extractOutline(filename, lines) {
8357
+ const ext = pathMod3.extname(filename).toLowerCase();
8358
+ const lang = EXT_TO_LANG[ext];
8359
+ if (!lang) return [];
8360
+ switch (lang) {
8361
+ case "ts":
8362
+ return extractTs(lines);
8363
+ case "py":
8364
+ return extractPython(lines);
8365
+ case "go":
8366
+ return extractGo(lines);
8367
+ case "rust":
8368
+ return extractRust(lines);
8369
+ case "md":
8370
+ return extractMarkdown(lines);
8371
+ }
8372
+ }
8373
+ function extractTs(lines) {
8374
+ const out = [];
8375
+ for (let i = 0; i < lines.length; i++) {
8376
+ const line = lines[i];
8377
+ if (!line.startsWith("export ")) continue;
8378
+ const m = TS_EXPORT_RE.exec(line);
8379
+ if (!m) continue;
8380
+ out.push({ line: i + 1, text: `export ${m[1]} ${m[2]}` });
8381
+ }
8382
+ return out;
8383
+ }
8384
+ function extractPython(lines) {
8385
+ const out = [];
8386
+ for (let i = 0; i < lines.length; i++) {
8387
+ const line = lines[i];
8388
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
8389
+ const m = PY_DECL_RE.exec(line);
8390
+ if (!m) continue;
8391
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8392
+ }
8393
+ return out;
8394
+ }
8395
+ function extractGo(lines) {
8396
+ const out = [];
8397
+ for (let i = 0; i < lines.length; i++) {
8398
+ const line = lines[i];
8399
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
8400
+ const m = GO_DECL_RE.exec(line);
8401
+ if (!m) continue;
8402
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8403
+ }
8404
+ return out;
8405
+ }
8406
+ function extractRust(lines) {
8407
+ const out = [];
8408
+ for (let i = 0; i < lines.length; i++) {
8409
+ const line = lines[i];
8410
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
8411
+ const implMatch = RUST_IMPL_RE.exec(line);
8412
+ if (implMatch) {
8413
+ out.push({ line: i + 1, text: `impl ${implMatch[1]}` });
8414
+ continue;
8415
+ }
8416
+ const m = RUST_DECL_RE.exec(line);
8417
+ if (!m) continue;
8418
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8419
+ }
8420
+ return out;
8421
+ }
8422
+ function extractMarkdown(lines) {
8423
+ const out = [];
8424
+ let inFence = false;
8425
+ for (let i = 0; i < lines.length; i++) {
8426
+ const line = lines[i];
8427
+ if (MD_FENCE_RE.test(line)) {
8428
+ inFence = !inFence;
8429
+ continue;
8430
+ }
8431
+ if (inFence) continue;
8432
+ const m = MD_HEADING_RE.exec(line);
8433
+ if (!m) continue;
8434
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
8435
+ }
8436
+ return out;
8437
+ }
8438
+ function formatOutline(entries) {
8439
+ const total = entries.length;
8440
+ if (total === 0) return "";
8441
+ const lastEntry = entries[total - 1];
8442
+ const width = String(lastEntry.line).length;
8443
+ const fmt = (e) => ` L${String(e.line).padStart(width, " ")} ${e.text}`;
8444
+ const header = `[outline: ${total} symbol${total === 1 ? "" : "s"}]`;
8445
+ if (total <= OUTLINE_MAX_ENTRIES) {
8446
+ return [header, ...entries.map(fmt)].join("\n");
8447
+ }
8448
+ const headCount = OUTLINE_MAX_ENTRIES - OUTLINE_TAIL_KEEP;
8449
+ const headEntries = entries.slice(0, headCount);
8450
+ const tailEntries = entries.slice(-OUTLINE_TAIL_KEEP);
8451
+ const omitted = total - OUTLINE_MAX_ENTRIES;
8452
+ const gapStart = headEntries[headEntries.length - 1].line;
8453
+ const gapEnd = tailEntries[0].line;
8454
+ return [
8455
+ header,
8456
+ ...headEntries.map(fmt),
8457
+ ` [\u2026 ${omitted} more symbol${omitted === 1 ? "" : "s"} between L${gapStart} and L${gapEnd} \u2026]`,
8458
+ ...tailEntries.map(fmt)
8459
+ ].join("\n");
8460
+ }
8461
+
8091
8462
  // src/tools/fs/search.ts
8092
8463
  import { promises as fs3 } from "fs";
8093
- import * as pathMod3 from "path";
8464
+ import * as pathMod4 from "path";
8094
8465
  function throwIfAborted(signal) {
8095
8466
  if (!signal?.aborted) return;
8096
8467
  throw new DOMException("search aborted by user", "AbortError");
8097
8468
  }
8098
8469
  function displayRel3(rootDir, full) {
8099
- return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
8470
+ return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
8100
8471
  }
8101
8472
  async function searchFiles(ctx, startAbs, args) {
8102
8473
  throwIfAborted(args.signal);
@@ -8120,7 +8491,7 @@ async function searchFiles(ctx, startAbs, args) {
8120
8491
  }
8121
8492
  for (const e of entries) {
8122
8493
  throwIfAborted(args.signal);
8123
- const full = pathMod3.join(dir, e.name);
8494
+ const full = pathMod4.join(dir, e.name);
8124
8495
  const lower = e.name.toLowerCase();
8125
8496
  const hit = re ? re.test(e.name) : lower.includes(needle);
8126
8497
  if (hit) {
@@ -8199,11 +8570,11 @@ async function searchContent(ctx, startAbs, args) {
8199
8570
  throwIfAborted(args.signal);
8200
8571
  if (e.isDirectory()) {
8201
8572
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
8202
- await walk2(pathMod3.join(dir, e.name));
8573
+ await walk2(pathMod4.join(dir, e.name));
8203
8574
  continue;
8204
8575
  }
8205
8576
  if (!e.isFile()) continue;
8206
- const full = pathMod3.join(dir, e.name);
8577
+ const full = pathMod4.join(dir, e.name);
8207
8578
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel3(ctx.rootDir, full))) continue;
8208
8579
  if (ctx.isBinaryByName(e.name)) continue;
8209
8580
  let fh;
@@ -8300,47 +8671,11 @@ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
8300
8671
  var DEFAULT_AUTO_PREVIEW_LINES = 200;
8301
8672
  var AUTO_PREVIEW_HEAD_LINES = 80;
8302
8673
  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
- }
8674
+ var OUTLINE_MAX_ENTRIES2 = 30;
8340
8675
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
8341
8676
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
8342
8677
  function displayRel4(rootDir, full) {
8343
- return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
8678
+ return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
8344
8679
  }
8345
8680
  var GLOB_METACHARS = /[*?{[]/;
8346
8681
  function compileNameFilter(filter) {
@@ -8359,16 +8694,16 @@ function isLikelyBinaryByName(name) {
8359
8694
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
8360
8695
  }
8361
8696
  function registerFilesystemTools(registry, opts) {
8362
- const rootDir = pathMod4.resolve(opts.rootDir);
8697
+ const rootDir = pathMod5.resolve(opts.rootDir);
8363
8698
  const allowWriting = opts.allowWriting !== false;
8364
8699
  const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
8365
8700
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
8366
- const normRoot = pathMod4.resolve(rootDir);
8701
+ const normRoot = pathMod5.resolve(rootDir);
8367
8702
  const sessionApproved = /* @__PURE__ */ new Set();
8368
8703
  const inflightGate = /* @__PURE__ */ new Map();
8369
8704
  function pathIsUnder(child, parent) {
8370
- const rel = pathMod4.relative(parent, child);
8371
- return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
8705
+ const rel = pathMod5.relative(parent, child);
8706
+ return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
8372
8707
  }
8373
8708
  function looksLikeAbsoluteSystemPath(raw) {
8374
8709
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -8384,7 +8719,7 @@ function registerFilesystemTools(registry, opts) {
8384
8719
  if (pathIsUnder(abs, dir)) return;
8385
8720
  }
8386
8721
  const stat2 = await safeLstat(abs);
8387
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod4.dirname(abs);
8722
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod5.dirname(abs);
8388
8723
  let pending = inflightGate.get(allowPrefix);
8389
8724
  if (!pending) {
8390
8725
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -8412,7 +8747,7 @@ function registerFilesystemTools(registry, opts) {
8412
8747
  throw new Error("path must be a non-empty string");
8413
8748
  }
8414
8749
  if (looksLikeAbsoluteSystemPath(raw)) {
8415
- const abs = pathMod4.resolve(raw);
8750
+ const abs = pathMod5.resolve(raw);
8416
8751
  if (pathIsUnder(abs, normRoot)) return abs;
8417
8752
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
8418
8753
  return abs;
@@ -8422,7 +8757,7 @@ function registerFilesystemTools(registry, opts) {
8422
8757
  normalized = normalized.slice(1);
8423
8758
  }
8424
8759
  if (normalized.length === 0) normalized = ".";
8425
- const resolved = pathMod4.resolve(rootDir, normalized);
8760
+ const resolved = pathMod5.resolve(rootDir, normalized);
8426
8761
  if (!pathIsUnder(resolved, normRoot)) {
8427
8762
  throw new Error(
8428
8763
  `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 +8779,7 @@ function registerFilesystemTools(registry, opts) {
8444
8779
  - head: N \u2192 first N lines (imports, public API, small configs)
8445
8780
  - tail: N \u2192 last N lines (recently-added code, log tails)
8446
8781
  - 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.`,
8782
+ 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
8783
  readOnly: true,
8449
8784
  stormExempt: true,
8450
8785
  parameters: {
@@ -8512,7 +8847,7 @@ ${slice.join("\n")}`;
8512
8847
  const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
8513
8848
  const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
8514
8849
  const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
8515
- const outline = formatOutline(extractTsExportOutline(lines));
8850
+ const outline = formatOutline(extractOutline(abs, lines));
8516
8851
  const parts = [
8517
8852
  `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
8518
8853
  head
@@ -8620,7 +8955,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8620
8955
  lines.push(line);
8621
8956
  emitted++;
8622
8957
  if (e.isDirectory() && !skip) {
8623
- await walk2(pathMod4.join(dir, e.name), depth + 1);
8958
+ await walk2(pathMod5.join(dir, e.name), depth + 1);
8624
8959
  }
8625
8960
  }
8626
8961
  };
@@ -8780,7 +9115,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8780
9115
  },
8781
9116
  fn: async (args, ctx) => {
8782
9117
  const abs = await safePath(args.path, "write_file", ctx, "write");
8783
- await fs4.mkdir(pathMod4.dirname(abs), { recursive: true });
9118
+ await fs4.mkdir(pathMod5.dirname(abs), { recursive: true });
8784
9119
  await fs4.writeFile(abs, args.content, "utf8");
8785
9120
  return `wrote ${args.content.length} chars to ${displayRel4(rootDir, abs)}`;
8786
9121
  }
@@ -8866,7 +9201,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8866
9201
  fn: async (args, ctx) => {
8867
9202
  const src = await safePath(args.source, "move_file", ctx, "write");
8868
9203
  const dst = await safePath(args.destination, "move_file", ctx, "write");
8869
- await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
9204
+ await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
8870
9205
  await fs4.rename(src, dst);
8871
9206
  return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
8872
9207
  }
@@ -8934,7 +9269,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8934
9269
  fn: async (args, ctx) => {
8935
9270
  const src = await safePath(args.source, "copy_file", ctx);
8936
9271
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
8937
- await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
9272
+ await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
8938
9273
  await fs4.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
8939
9274
  return `copied ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
8940
9275
  }
@@ -8946,6 +9281,16 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
8946
9281
  function registerMemoryTools(registry, opts = {}) {
8947
9282
  const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
8948
9283
  const hasProject = store.hasProjectScope();
9284
+ const registry_types = loadMemoryTypeRegistry();
9285
+ const customTypeNames = registry_types.filter((r) => !r.builtin).map((r) => r.name);
9286
+ const typeDescParts = [
9287
+ "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
9288
+ ];
9289
+ if (customTypeNames.length > 0) {
9290
+ typeDescParts.push(
9291
+ `Custom types declared in config: ${customTypeNames.join(", ")}. Any string is accepted; unknown types are stored verbatim and treated as 'reference' priority.`
9292
+ );
9293
+ }
8949
9294
  registry.register({
8950
9295
  name: "remember",
8951
9296
  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 +9299,7 @@ function registerMemoryTools(registry, opts = {}) {
8954
9299
  properties: {
8955
9300
  type: {
8956
9301
  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."
9302
+ description: typeDescParts.join(" ")
8959
9303
  },
8960
9304
  scope: {
8961
9305
  type: "string",
@@ -8973,6 +9317,16 @@ function registerMemoryTools(registry, opts = {}) {
8973
9317
  content: {
8974
9318
  type: "string",
8975
9319
  description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
9320
+ },
9321
+ priority: {
9322
+ type: "string",
9323
+ enum: ["low", "medium", "high"],
9324
+ 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."
9325
+ },
9326
+ expires: {
9327
+ type: "string",
9328
+ enum: ["project_end"],
9329
+ description: "Optional lifecycle hint. `project_end` causes `/memory clear project` to also remove this entry even when it's stored at global scope."
8976
9330
  }
8977
9331
  },
8978
9332
  required: ["type", "scope", "name", "description", "content"]
@@ -8989,7 +9343,9 @@ function registerMemoryTools(registry, opts = {}) {
8989
9343
  type: args.type,
8990
9344
  scope: args.scope,
8991
9345
  description: args.description,
8992
- body: args.content
9346
+ body: args.content,
9347
+ ...args.priority ? { priority: args.priority } : {},
9348
+ ...args.expires ? { expires: args.expires } : {}
8993
9349
  });
8994
9350
  const key = sanitizeMemoryName(args.name);
8995
9351
  return [
@@ -9584,9 +9940,7 @@ ${NEGATIVE_CLAIM_RULE}
9584
9940
 
9585
9941
  ${TUI_FORMATTING_RULES}`;
9586
9942
  var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
9587
- var DEFAULT_MAX_ITERS = 16;
9588
- var MIN_MAX_ITERS = 1;
9589
- var MAX_MAX_ITERS = 32;
9943
+ var DEFAULT_PAUSE_EVERY = 16;
9590
9944
  var BUDGET_WARN_THRESHOLD = 3;
9591
9945
  function budgetParagraph(maxToolIters) {
9592
9946
  return `Tool budget: you have ${maxToolIters} tool call${maxToolIters === 1 ? "" : "s"} for this task. The cap is enforced from outside \u2014 the call after #${maxToolIters} is refused. Pace yourself: if you can't fully resolve the task within the budget, stop early and return what you have plus what's missing, rather than burning the budget on one branch.`;
@@ -9609,12 +9963,13 @@ function subagentBudgetHint(spawnCount, totalTokens) {
9609
9963
  }
9610
9964
  async function spawnSubagent(opts) {
9611
9965
  const model = opts.model ?? DEFAULT_SUBAGENT_MODEL;
9612
- const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
9966
+ const maxToolIters = opts.maxToolIters ?? DEFAULT_PAUSE_EVERY;
9613
9967
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
9614
9968
  const sink = opts.sink;
9615
9969
  const skillName = opts.skillName;
9616
- const startedAt = Date.now();
9617
9970
  const runId = nextRunId();
9971
+ const sessionName = opts.resumeSession ?? `subagent-${runId}-${timestampSuffix()}`;
9972
+ const startedAt = Date.now();
9618
9973
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
9619
9974
  sink?.current?.({
9620
9975
  kind: "start",
@@ -9694,10 +10049,9 @@ ${budgetParagraph(maxToolIters)}`,
9694
10049
  reasoningEffort: DEFAULT_SUBAGENT_EFFORT,
9695
10050
  maxToolIters,
9696
10051
  hooks: [],
9697
- // Streaming on so the parent UI can flip the "summarising" phase the
9698
- // moment the model starts emitting the final answer (first assistant_delta
9699
- // after the last tool result, before assistant_final lands).
9700
- stream: true
10052
+ stream: true,
10053
+ session: sessionName,
10054
+ onIterBudgetExhausted: "pause"
9701
10055
  });
9702
10056
  const onParentAbort = () => childLoop.abort();
9703
10057
  if (opts.parentSignal?.aborted) {
@@ -9709,8 +10063,13 @@ ${budgetParagraph(maxToolIters)}`,
9709
10063
  let errorMessage;
9710
10064
  let toolIter = 0;
9711
10065
  let summarisingEmitted = false;
10066
+ let paused = false;
10067
+ let partialSummary;
10068
+ const taskForLoop = opts.resumeSession ? `[Resume: your tool-call budget has been refreshed with ${maxToolIters} new calls. Earlier "wrap up" / "finalize NOW" budget hints in this conversation referred to the previous window \u2014 they no longer apply. Continue the work you were given.]
10069
+
10070
+ ${opts.task}` : opts.task;
9712
10071
  try {
9713
- for await (const ev of childLoop.step(opts.task)) {
10072
+ for await (const ev of childLoop.step(taskForLoop)) {
9714
10073
  sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
9715
10074
  if (ev.role === "tool") {
9716
10075
  toolIter++;
@@ -9748,13 +10107,17 @@ ${budgetParagraph(maxToolIters)}`,
9748
10107
  if (ev.role === "error") {
9749
10108
  errorMessage = ev.error ?? "subagent error";
9750
10109
  }
10110
+ if (ev.role === "paused") {
10111
+ paused = true;
10112
+ if (ev.partialSummary) partialSummary = ev.partialSummary;
10113
+ }
9751
10114
  }
9752
10115
  } catch (err) {
9753
10116
  errorMessage = err.message;
9754
10117
  } finally {
9755
10118
  opts.parentSignal?.removeEventListener("abort", onParentAbort);
9756
10119
  }
9757
- if (!errorMessage && !final) {
10120
+ if (!errorMessage && !final && !paused) {
9758
10121
  errorMessage = opts.parentSignal?.aborted ? "subagent aborted before producing an answer" : "subagent ended without producing an answer";
9759
10122
  }
9760
10123
  const elapsedMs = Date.now() - startedAt;
@@ -9788,7 +10151,10 @@ ${budgetParagraph(maxToolIters)}`,
9788
10151
  costUsd: costUsd2,
9789
10152
  model,
9790
10153
  skillName,
9791
- usage
10154
+ usage,
10155
+ paused: paused || void 0,
10156
+ pausedSession: paused ? sessionName : void 0,
10157
+ partialSummary: paused ? partialSummary : void 0
9792
10158
  };
9793
10159
  }
9794
10160
  function aggregateChildUsage(loop) {
@@ -9803,6 +10169,18 @@ function aggregateChildUsage(loop) {
9803
10169
  return agg;
9804
10170
  }
9805
10171
  function formatSubagentResult(r) {
10172
+ if (r.paused) {
10173
+ return JSON.stringify({
10174
+ success: false,
10175
+ paused: true,
10176
+ resume_session: r.pausedSession,
10177
+ tool_iters: r.toolIters,
10178
+ elapsed_ms: r.elapsedMs,
10179
+ cost_usd: r.costUsd,
10180
+ partial_summary: r.partialSummary,
10181
+ note: `Subagent reached its pause-every interval (${r.toolIters} tool calls) without producing a final answer. Read partial_summary above to see what was done / left / blocked, then decide: resume by calling spawn_subagent again with resume_session="${r.pausedSession}" (the task arg becomes a continuation nudge \u2014 e.g. "finish what you started"), or accept the partial work and proceed with what you already know.`
10182
+ });
10183
+ }
9806
10184
  if (!r.success) {
9807
10185
  return JSON.stringify({
9808
10186
  success: false,
@@ -9825,7 +10203,7 @@ function registerSubagentTool(parentRegistry, opts) {
9825
10203
  const baseSystem = opts.defaultSystem ?? SUBAGENT_BASE_SYSTEM;
9826
10204
  const defaultSystemBase = opts.projectRoot ? applyProjectMemory(baseSystem, opts.projectRoot) : baseSystem;
9827
10205
  const defaultModel = opts.defaultModel ?? DEFAULT_SUBAGENT_MODEL;
9828
- const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
10206
+ const maxToolIters = opts.maxToolIters ?? DEFAULT_PAUSE_EVERY;
9829
10207
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
9830
10208
  const sink = opts.sink;
9831
10209
  let sessionSpawnCount = 0;
@@ -9833,17 +10211,17 @@ function registerSubagentTool(parentRegistry, opts) {
9833
10211
  parentRegistry.register({
9834
10212
  name: SUBAGENT_TOOL_NAME,
9835
10213
  parallelSafe: true,
9836
- description: "Spawn an isolated subagent to handle a self-contained subtask in a fresh context, returning only its final answer. **Prefer direct tools.** Spawn primarily for parallel fan-out (2+ independent investigations issued in one tool batch) or when the work would otherwise need >10 file reads/searches whose trail you don't need to keep. Single greps, 1-3 file cross-references, and 'keep my context clean for one question' are NOT good reasons to spawn \u2014 direct tools are cheaper and let you reference the evidence later. Each spawn pays a fresh prefix-cache miss plus a full child loop. The subagent inherits your tools but runs in its own isolated message log; only the final assistant message comes back. Keep tasks focused; the subagent has a stricter iter budget than you do.",
10214
+ description: "Spawn an isolated subagent to handle a self-contained subtask in a fresh context, returning only its final answer. **Prefer direct tools.** Spawn primarily for parallel fan-out (2+ independent investigations issued in one tool batch) or when the work would otherwise need >10 file reads/searches whose trail you don't need to keep. Single greps, 1-3 file cross-references, and 'keep my context clean for one question' are NOT good reasons to spawn \u2014 direct tools are cheaper and let you reference the evidence later. Each fresh spawn pays a prefix-cache miss plus a full child loop. The subagent inherits your tools but runs in its own isolated message log; only the final assistant message comes back. **Pause/resume**: subagents yield back to you every `max_iters` tool calls. A paused result returns `{ paused: true, resume_session: \"...\" }` \u2014 you can either accept the partial state, or call spawn_subagent again with `resume_session` set to continue where it left off (cheap: the prior prefix is cached). Tasks expected to run much longer than 16 tool calls don't need a huge `max_iters` \u2014 just resume.",
9837
10215
  parameters: {
9838
10216
  type: "object",
9839
10217
  properties: {
9840
10218
  task: {
9841
10219
  type: "string",
9842
- description: "The subtask the subagent should perform. Be specific and self-contained \u2014 the subagent has none of your conversation context, only what you write here."
10220
+ description: 'The subtask the subagent should perform. Be specific and self-contained \u2014 the subagent has none of your conversation context, only what you write here. When resuming via `resume_session`, this becomes a continuation nudge (e.g. "finish what you started" or a delta instruction).'
9843
10221
  },
9844
10222
  system: {
9845
10223
  type: "string",
9846
- description: "Optional override for the subagent's system prompt. The default tells it to stay focused and return a concise answer; override only when the subtask needs a specialized persona."
10224
+ description: "Optional override for the subagent's system prompt. The default tells it to stay focused and return a concise answer; override only when the subtask needs a specialized persona. Ignored on resume \u2014 the prior session keeps its original system prompt for cache stability."
9847
10225
  },
9848
10226
  model: {
9849
10227
  type: "string",
@@ -9852,9 +10230,11 @@ function registerSubagentTool(parentRegistry, opts) {
9852
10230
  },
9853
10231
  max_iters: {
9854
10232
  type: "integer",
9855
- minimum: MIN_MAX_ITERS,
9856
- maximum: MAX_MAX_ITERS,
9857
- description: `Cap on the subagent's tool-call iterations. Default 16 (or the type's default when 'type' is set). Hard range: ${MIN_MAX_ITERS}-${MAX_MAX_ITERS}; out-of-range values are clamped to the nearest end.`
10233
+ description: "How many tool calls the subagent runs before pausing back to you for a checkpoint. Default 16. This is checkpoint cadence, not a budget \u2014 work continues across pauses via `resume_session`. Pick what feels natural for the granularity of decisions you want to make: small (4-8) when you want frequent control, larger (32-64) when the subagent should mostly run autonomously."
10234
+ },
10235
+ resume_session: {
10236
+ type: "string",
10237
+ description: "Provide the `resume_session` value returned by a previous paused spawn to continue that subagent. When set, prior messages are loaded from disk and the original system prompt is reused (cache-friendly). `task` becomes a continuation nudge."
9858
10238
  },
9859
10239
  type: {
9860
10240
  type: "string",
@@ -9876,7 +10256,8 @@ function registerSubagentTool(parentRegistry, opts) {
9876
10256
  const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : typeSpec?.system ?? `${defaultSystemBase}
9877
10257
 
9878
10258
  ${escalationContract(model)}`;
9879
- const callerIters = clampMaxIters(args.max_iters);
10259
+ const callerIters = parseMaxIters(args.max_iters);
10260
+ const resumeSession = typeof args.resume_session === "string" && args.resume_session.trim().length > 0 ? args.resume_session.trim() : void 0;
9880
10261
  const result = await spawnSubagent({
9881
10262
  client: opts.client,
9882
10263
  parentRegistry,
@@ -9886,7 +10267,8 @@ ${escalationContract(model)}`;
9886
10267
  maxToolIters: callerIters ?? typeSpec?.maxToolIters ?? maxToolIters,
9887
10268
  maxResultChars,
9888
10269
  sink,
9889
- parentSignal: ctx?.signal
10270
+ parentSignal: ctx?.signal,
10271
+ resumeSession
9890
10272
  });
9891
10273
  sessionSpawnCount++;
9892
10274
  sessionSpawnTokens += result.usage.totalTokens;
@@ -9898,12 +10280,10 @@ ${hint}` : formatted;
9898
10280
  });
9899
10281
  return parentRegistry;
9900
10282
  }
9901
- function clampMaxIters(raw) {
10283
+ function parseMaxIters(raw) {
9902
10284
  if (typeof raw !== "number" || !Number.isFinite(raw)) return void 0;
9903
10285
  const n = Math.floor(raw);
9904
- if (n < MIN_MAX_ITERS) return MIN_MAX_ITERS;
9905
- if (n > MAX_MAX_ITERS) return MAX_MAX_ITERS;
9906
- return n;
10286
+ return n >= 1 ? n : void 0;
9907
10287
  }
9908
10288
  function forkRegistryExcluding(parent, exclude) {
9909
10289
  const child = new ToolRegistry();
@@ -9932,11 +10312,11 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
9932
10312
  }
9933
10313
 
9934
10314
  // src/tools/shell.ts
9935
- import * as pathMod8 from "path";
10315
+ import * as pathMod9 from "path";
9936
10316
 
9937
10317
  // src/tools/jobs.ts
9938
10318
  import { spawn as spawn2 } from "child_process";
9939
- import * as pathMod5 from "path";
10319
+ import * as pathMod6 from "path";
9940
10320
  function killProcessTree(pid, signal) {
9941
10321
  if (process.platform === "win32") {
9942
10322
  const args = ["/pid", String(pid), "/T"];
@@ -9996,7 +10376,7 @@ var JobRegistry = class {
9996
10376
  const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
9997
10377
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
9998
10378
  const spawnOpts = {
9999
- cwd: pathMod5.resolve(opts.cwd),
10379
+ cwd: pathMod6.resolve(opts.cwd),
10000
10380
  shell: false,
10001
10381
  windowsHide: true,
10002
10382
  env: process.env,
@@ -10109,12 +10489,15 @@ ${job.output.slice(start)}`;
10109
10489
  job.signalReady();
10110
10490
  job.signalClosed();
10111
10491
  });
10112
- child.on("close", (code) => {
10492
+ const settleClosed = (code) => {
10493
+ if (!job.running && job.exitCode !== null) return;
10113
10494
  job.running = false;
10114
10495
  job.exitCode = code;
10115
10496
  job.signalReady();
10116
10497
  job.signalClosed();
10117
- });
10498
+ };
10499
+ child.on("exit", settleClosed);
10500
+ child.on("close", settleClosed);
10118
10501
  const onAbort = () => this.stop(id, { graceMs: 100 });
10119
10502
  if (opts.signal?.aborted) {
10120
10503
  onAbort();
@@ -10171,21 +10554,26 @@ ${job.output.slice(start)}`;
10171
10554
  latestOutput: job.output
10172
10555
  };
10173
10556
  }
10174
- const timeoutMs = Math.max(0, Math.min(3e4, opts.timeoutMs ?? 5e3));
10557
+ const timeoutMs = Math.max(0, Math.min(3e5, opts.timeoutMs ?? 5e3));
10558
+ const waitFor = opts.waitFor ?? "exit";
10175
10559
  const startOutput = job.output;
10560
+ const racers = [job.closedPromise];
10176
10561
  let wakeOutput = null;
10177
- const outputPromise = new Promise((resolve10) => {
10178
- wakeOutput = resolve10;
10179
- job.outputWaiters.add(resolve10);
10180
- });
10562
+ if (waitFor === "output-or-exit") {
10563
+ racers.push(
10564
+ new Promise((resolve10) => {
10565
+ wakeOutput = resolve10;
10566
+ job.outputWaiters.add(resolve10);
10567
+ })
10568
+ );
10569
+ }
10181
10570
  let timer = null;
10182
- await Promise.race([
10183
- job.closedPromise,
10184
- outputPromise,
10571
+ racers.push(
10185
10572
  new Promise((resolve10) => {
10186
10573
  timer = setTimeout(resolve10, timeoutMs);
10187
10574
  })
10188
- ]);
10575
+ );
10576
+ await Promise.race(racers);
10189
10577
  if (timer) clearTimeout(timer);
10190
10578
  if (wakeOutput) job.outputWaiters.delete(wakeOutput);
10191
10579
  return {
@@ -10219,6 +10607,10 @@ ${job.output.slice(start)}`;
10219
10607
  }
10220
10608
  }
10221
10609
  await Promise.race([job.closedPromise, new Promise((res) => setTimeout(res, 5e3))]);
10610
+ if (job.running) {
10611
+ job.running = false;
10612
+ job.signalClosed();
10613
+ }
10222
10614
  }
10223
10615
  return snapshot(job);
10224
10616
  }
@@ -10252,6 +10644,12 @@ ${job.output.slice(start)}`;
10252
10644
  }
10253
10645
  const remaining = Math.max(800, deadlineMs - elapsed());
10254
10646
  await Promise.race([allClose, new Promise((res) => setTimeout(res, remaining))]);
10647
+ for (const job of runningJobs) {
10648
+ if (job.running) {
10649
+ job.running = false;
10650
+ job.signalClosed();
10651
+ }
10652
+ }
10255
10653
  }
10256
10654
  /** Count of still-running jobs — drives the TUI status-bar indicator. */
10257
10655
  runningCount() {
@@ -10282,12 +10680,13 @@ function latestOutputSince(before, after) {
10282
10680
  // src/tools/shell/exec.ts
10283
10681
  import { spawn as spawn4, spawnSync } from "child_process";
10284
10682
  import { existsSync as existsSync8, statSync as statSync5 } from "fs";
10285
- import * as pathMod7 from "path";
10683
+ import * as pathMod8 from "path";
10286
10684
 
10287
10685
  // src/tools/shell-chain.ts
10288
10686
  import { spawn as spawn3 } from "child_process";
10289
10687
  import { closeSync, openSync } from "fs";
10290
- import * as pathMod6 from "path";
10688
+ import { devNull } from "os";
10689
+ import * as pathMod7 from "path";
10291
10690
  var UnsupportedSyntaxError = class extends Error {
10292
10691
  constructor(detail) {
10293
10692
  super(`run_command: ${detail}`);
@@ -10546,6 +10945,12 @@ async function runChain(chain, opts) {
10546
10945
  [\u2026 truncated ${output.length - opts.maxOutputChars} chars \u2026]` : output;
10547
10946
  return { exitCode: lastExit, output: truncated, timedOut };
10548
10947
  }
10948
+ function isNullDeviceAlias(target) {
10949
+ const lower = target.toLowerCase();
10950
+ if (lower === "/dev/null") return true;
10951
+ if (process.platform === "win32" && lower === "nul") return true;
10952
+ return false;
10953
+ }
10549
10954
  function openRedirects(redirects, cwd) {
10550
10955
  let stdinFd = null;
10551
10956
  let stdoutFd = null;
@@ -10554,7 +10959,7 @@ function openRedirects(redirects, cwd) {
10554
10959
  let bothFd = null;
10555
10960
  const toClose = [];
10556
10961
  const open = (target, flags) => {
10557
- const resolved = pathMod6.resolve(cwd, target);
10962
+ const resolved = isNullDeviceAlias(target) ? devNull : pathMod7.resolve(cwd, target);
10558
10963
  const fd = openSync(resolved, flags);
10559
10964
  toClose.push(fd);
10560
10965
  return fd;
@@ -11051,16 +11456,16 @@ function resolveExecutable(cmd, opts = {}) {
11051
11456
  const platform = opts.platform ?? process.platform;
11052
11457
  if (platform !== "win32") return cmd;
11053
11458
  if (!cmd) return cmd;
11054
- if (cmd.includes("/") || cmd.includes("\\") || pathMod7.isAbsolute(cmd)) return cmd;
11055
- if (pathMod7.extname(cmd)) return cmd;
11459
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod8.isAbsolute(cmd)) return cmd;
11460
+ if (pathMod8.extname(cmd)) return cmd;
11056
11461
  const env = opts.env ?? process.env;
11057
11462
  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);
11463
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod8.delimiter);
11059
11464
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
11060
11465
  const isFile = opts.isFile ?? defaultIsFile;
11061
11466
  for (const dir of pathDirs) {
11062
11467
  for (const ext of pathExt) {
11063
- const full = pathMod7.win32.join(dir, cmd + ext);
11468
+ const full = pathMod8.win32.join(dir, cmd + ext);
11064
11469
  if (isFile(full)) return full;
11065
11470
  }
11066
11471
  }
@@ -11176,8 +11581,8 @@ function withUtf8Codepage(cmdline) {
11176
11581
  function isBareWindowsName(s) {
11177
11582
  if (!s) return false;
11178
11583
  if (s.includes("/") || s.includes("\\")) return false;
11179
- if (pathMod7.isAbsolute(s)) return false;
11180
- if (pathMod7.extname(s)) return false;
11584
+ if (pathMod8.isAbsolute(s)) return false;
11585
+ if (pathMod8.extname(s)) return false;
11181
11586
  return true;
11182
11587
  }
11183
11588
  function quoteForCmdExe(arg) {
@@ -11198,7 +11603,7 @@ var NeedsConfirmationError = class extends Error {
11198
11603
  }
11199
11604
  };
11200
11605
  function registerShellTools(registry, opts) {
11201
- const rootDir = pathMod8.resolve(opts.rootDir);
11606
+ const rootDir = pathMod9.resolve(opts.rootDir);
11202
11607
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
11203
11608
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
11204
11609
  const jobs = opts.jobs ?? new JobRegistry();
@@ -11264,7 +11669,7 @@ function registerShellTools(registry, opts) {
11264
11669
  });
11265
11670
  registry.register({
11266
11671
  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.",
11672
+ 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
11673
  parameters: {
11269
11674
  type: "object",
11270
11675
  properties: {
@@ -11337,7 +11742,7 @@ function registerShellTools(registry, opts) {
11337
11742
  });
11338
11743
  registry.register({
11339
11744
  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`.",
11745
+ 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
11746
  readOnly: true,
11342
11747
  parallelSafe: true,
11343
11748
  stormExempt: true,
@@ -11347,13 +11752,21 @@ function registerShellTools(registry, opts) {
11347
11752
  jobId: { type: "integer", description: "Job id returned by run_background." },
11348
11753
  timeoutMs: {
11349
11754
  type: "integer",
11350
- description: "Max time to block before returning if nothing changes. Clamped to 0..30000. Default 5000."
11755
+ description: "Max time to block before returning if the wake condition hasn't fired. Clamped to 0..300000. Default 5000."
11756
+ },
11757
+ waitFor: {
11758
+ type: "string",
11759
+ enum: ["exit", "output-or-exit"],
11760
+ 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
11761
  }
11352
11762
  },
11353
11763
  required: ["jobId"]
11354
11764
  },
11355
11765
  fn: async (args) => {
11356
- const out = await jobs.waitForJob(args.jobId, { timeoutMs: args.timeoutMs });
11766
+ const out = await jobs.waitForJob(args.jobId, {
11767
+ timeoutMs: args.timeoutMs,
11768
+ waitFor: args.waitFor
11769
+ });
11357
11770
  if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
11358
11771
  return {
11359
11772
  jobId: args.jobId,
@@ -12558,8 +12971,12 @@ var McpClient = class {
12558
12971
  }
12559
12972
  });
12560
12973
  promise.catch(() => void 0);
12974
+ const promiseSettled = promise.then(
12975
+ () => void 0,
12976
+ () => void 0
12977
+ );
12561
12978
  try {
12562
- await Promise.race([this.transport.send(frame), promise.then(() => void 0)]);
12979
+ await Promise.race([this.transport.send(frame), promiseSettled]);
12563
12980
  } catch (err) {
12564
12981
  const pending = this.pending.get(id);
12565
12982
  if (pending) clearTimeout(pending.timeout);
@@ -13502,13 +13919,15 @@ Do NOT try to switch via \`run_command\` (\`cd\`, \`pushd\`, etc.) \u2014 your t
13502
13919
  You have TWO tools for running shell commands, and picking the right one is non-negotiable:
13503
13920
 
13504
13921
  - \`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\`.
13922
+ - \`run_background\` \u2014 spawns and detaches after a brief startup window. Use for:
13923
+ - **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\`.
13924
+ - **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
13925
 
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.
13926
+ **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
13927
 
13509
13928
  After \`run_background\`, tools available to you:
13510
13929
  - \`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.
13930
+ - \`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
13931
  - \`list_jobs\` \u2014 see every job this session (running + exited).
13513
13932
  - \`stop_job(jobId)\` \u2014 SIGTERM \u2192 SIGKILL after grace. Stop before switching port / config.
13514
13933