reasonix 0.34.1 → 0.35.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 (52) hide show
  1. package/dashboard/app.css +16 -14
  2. package/dashboard/dist/app.js +35 -15
  3. package/dashboard/dist/app.js.map +1 -1
  4. package/dist/cli/{chat-TD6GR3QK.js → chat-AB5D7I3V.js} +9 -9
  5. package/dist/cli/{chunk-EINEIIIW.js → chunk-GPHBJWCV.js} +278 -91
  6. package/dist/cli/chunk-GPHBJWCV.js.map +1 -0
  7. package/dist/cli/{chunk-F3ILWP2L.js → chunk-IDP65VCC.js} +2 -2
  8. package/dist/cli/{chunk-OERAGRJX.js → chunk-JJTOZPM3.js} +2 -2
  9. package/dist/cli/{chunk-U3V2ZQ5J.js → chunk-KJQIA4US.js} +6 -2
  10. package/dist/cli/chunk-KJQIA4US.js.map +1 -0
  11. package/dist/cli/{chunk-LNTORE5K.js → chunk-N2IC4XDL.js} +149 -35
  12. package/dist/cli/chunk-N2IC4XDL.js.map +1 -0
  13. package/dist/cli/{chunk-SA4UGZPG.js → chunk-RJ5GUVS2.js} +6 -1
  14. package/dist/cli/chunk-RJ5GUVS2.js.map +1 -0
  15. package/dist/cli/{chunk-Q36KBLSU.js → chunk-SN7YH6FC.js} +125 -1
  16. package/dist/cli/chunk-SN7YH6FC.js.map +1 -0
  17. package/dist/cli/{chunk-6TMHAK5D.js → chunk-ZU45XW3P.js} +2 -2
  18. package/dist/cli/code-XBEFHXVM.js +433 -0
  19. package/dist/cli/code-XBEFHXVM.js.map +1 -0
  20. package/dist/cli/{doctor-YASM64X6.js → doctor-A565GMWD.js} +4 -4
  21. package/dist/cli/index.js +15 -15
  22. package/dist/cli/{prompt-V47QKSAR.js → prompt-YEKXMNNV.js} +3 -3
  23. package/dist/cli/{replay-JEDLU7F2.js → replay-P2WC5N5X.js} +2 -2
  24. package/dist/cli/replay-P2WC5N5X.js.map +1 -0
  25. package/dist/cli/{run-NHD2RSTD.js → run-QBWJETS3.js} +6 -6
  26. package/dist/cli/{server-MC4A4WAJ.js → server-SMLVXIW4.js} +5 -5
  27. package/dist/cli/{sessions-ZHWJEW4L.js → sessions-55RIZVWG.js} +6 -6
  28. package/dist/cli/{setup-DK43MT47.js → setup-QXMONZ4P.js} +2 -2
  29. package/dist/cli/{version-O362UKPM.js → version-Q2HA3AAC.js} +6 -6
  30. package/dist/index.d.ts +5 -0
  31. package/dist/index.js +273 -31
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/dist/cli/chunk-EINEIIIW.js.map +0 -1
  35. package/dist/cli/chunk-LNTORE5K.js.map +0 -1
  36. package/dist/cli/chunk-Q36KBLSU.js.map +0 -1
  37. package/dist/cli/chunk-SA4UGZPG.js.map +0 -1
  38. package/dist/cli/chunk-U3V2ZQ5J.js.map +0 -1
  39. package/dist/cli/code-TGUOQBRJ.js +0 -153
  40. package/dist/cli/code-TGUOQBRJ.js.map +0 -1
  41. package/dist/cli/replay-JEDLU7F2.js.map +0 -1
  42. /package/dist/cli/{chat-TD6GR3QK.js.map → chat-AB5D7I3V.js.map} +0 -0
  43. /package/dist/cli/{chunk-F3ILWP2L.js.map → chunk-IDP65VCC.js.map} +0 -0
  44. /package/dist/cli/{chunk-OERAGRJX.js.map → chunk-JJTOZPM3.js.map} +0 -0
  45. /package/dist/cli/{chunk-6TMHAK5D.js.map → chunk-ZU45XW3P.js.map} +0 -0
  46. /package/dist/cli/{doctor-YASM64X6.js.map → doctor-A565GMWD.js.map} +0 -0
  47. /package/dist/cli/{prompt-V47QKSAR.js.map → prompt-YEKXMNNV.js.map} +0 -0
  48. /package/dist/cli/{run-NHD2RSTD.js.map → run-QBWJETS3.js.map} +0 -0
  49. /package/dist/cli/{server-MC4A4WAJ.js.map → server-SMLVXIW4.js.map} +0 -0
  50. /package/dist/cli/{sessions-ZHWJEW4L.js.map → sessions-55RIZVWG.js.map} +0 -0
  51. /package/dist/cli/{setup-DK43MT47.js.map → setup-QXMONZ4P.js.map} +0 -0
  52. /package/dist/cli/{version-O362UKPM.js.map → version-Q2HA3AAC.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1188,6 +1188,68 @@ var EN = {
1188
1188
  selectFooter: "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel",
1189
1189
  stepCounter: "Step {step}/{total} \xB7 "
1190
1190
  },
1191
+ planFlow: {
1192
+ approveCardTitle: "Approve plan",
1193
+ approveCardMetaRight: "awaiting",
1194
+ openQuestionsBanner: "\u25B2 the plan flags open questions or risks \u2014 pick {refine} to write concrete answers before the model moves on.",
1195
+ openQuestionsHeader: "Open questions / risks",
1196
+ truncatedBodyMore: "\u2026 {n} more line above in scrollback",
1197
+ truncatedBodyMorePlural: "\u2026 {n} more lines above in scrollback",
1198
+ picker: {
1199
+ accept: "accept",
1200
+ acceptHint: "run it now, in order",
1201
+ refine: "refine",
1202
+ refineHint: "give the agent more guidance, draft a new plan",
1203
+ revise: "revise",
1204
+ reviseHint: "edit the plan inline before running (skip / reorder steps)",
1205
+ reject: "reject",
1206
+ rejectHint: "discard, agent will retry from scratch"
1207
+ },
1208
+ refineFooter: "\u23CE send \xB7 esc return to picker",
1209
+ refineQuestionsHeading: "Answer these or describe the change you want:",
1210
+ modes: {
1211
+ approve: {
1212
+ title: "approving \u2014 any last instructions?",
1213
+ hint: "Answer questions the plan raised, add constraints, or just press Enter to approve as-is.",
1214
+ blankHint: " (Enter with blank = approve without extra instructions.)"
1215
+ },
1216
+ refine: {
1217
+ title: "refining \u2014 what should the model change?",
1218
+ hint: "Describe what's wrong or missing, or answer questions the plan raised.",
1219
+ blankHint: " (Enter with blank = let the model pick safe defaults for any open questions.)"
1220
+ },
1221
+ reject: {
1222
+ title: "rejecting \u2014 tell the model why (optional)",
1223
+ hint: "Say what the model got wrong about your goal, or what you actually want instead.",
1224
+ blankHint: " (Enter with blank = cancel without explanation; the model will ask what you want.)"
1225
+ },
1226
+ "checkpoint-revise": {
1227
+ title: "revising \u2014 what should change before the next step?",
1228
+ hint: "Scope change, skip steps, alternative approach \u2014 the model adjusts the remaining plan.",
1229
+ blankHint: " (Enter with blank = continue with the current plan.)"
1230
+ },
1231
+ "choice-custom": {
1232
+ title: "custom answer \u2014 type whatever fits",
1233
+ hint: "Free-form reply. The model reads it verbatim and proceeds \u2014 no need to match the listed options.",
1234
+ blankHint: " (Enter with blank = ask the model what you actually want.)"
1235
+ }
1236
+ },
1237
+ checkpoint: {
1238
+ title: "Checkpoint \u2014 step done",
1239
+ continue: "Continue \u2014 run the next step",
1240
+ continueHint: "Model resumes with the next step.",
1241
+ revise: "Revise \u2014 give feedback before the next step",
1242
+ reviseHint: "Stay paused, type guidance; model adjusts the remaining plan.",
1243
+ stop: "Stop \u2014 end the plan here",
1244
+ stopHint: "Model summarizes what was done and ends."
1245
+ },
1246
+ stepList: {
1247
+ counter: "{total} steps",
1248
+ counterSingular: "{total} step",
1249
+ counterDone: "{done}/{total} done ({pct}%) \xB7 {total} steps",
1250
+ counterDoneSingular: "{done}/{total} done ({pct}%) \xB7 {total} step"
1251
+ }
1252
+ },
1191
1253
  app: {
1192
1254
  walkCancelledRemaining: "\u25B8 walk cancelled \u2014 {count} block(s) still pending.",
1193
1255
  walkCancelled: "\u25B8 walk cancelled.",
@@ -1973,6 +2035,68 @@ var zhCN = {
1973
2035
  selectFooter: "[\u2191\u2193] \u79FB\u52A8 \xB7 [Enter] \u786E\u8BA4 \xB7 [Esc] \u53D6\u6D88",
1974
2036
  stepCounter: "\u6B65\u9AA4 {step}/{total} \xB7 "
1975
2037
  },
2038
+ planFlow: {
2039
+ approveCardTitle: "\u786E\u8BA4\u8BA1\u5212",
2040
+ approveCardMetaRight: "\u7B49\u5F85\u4E2D",
2041
+ openQuestionsBanner: "\u25B2 \u8BA1\u5212\u4E2D\u6807\u8BB0\u4E86\u5F85\u786E\u8BA4\u7684\u95EE\u9898\u6216\u98CE\u9669 \u2014\u2014 \u8BF7\u9009 {refine} \u7ED9\u51FA\u660E\u786E\u7B54\u6848\uFF0C\u518D\u8BA9\u6A21\u578B\u7EE7\u7EED\u3002",
2042
+ openQuestionsHeader: "\u5F85\u786E\u8BA4 / \u98CE\u9669",
2043
+ truncatedBodyMore: "\u2026 \u8FD8\u6709 {n} \u884C\u5728\u4E0A\u65B9\u6EDA\u52A8\u5386\u53F2\u4E2D",
2044
+ truncatedBodyMorePlural: "\u2026 \u8FD8\u6709 {n} \u884C\u5728\u4E0A\u65B9\u6EDA\u52A8\u5386\u53F2\u4E2D",
2045
+ picker: {
2046
+ accept: "\u91C7\u7EB3",
2047
+ acceptHint: "\u7ACB\u5373\u6309\u987A\u5E8F\u6267\u884C",
2048
+ refine: "\u7EC6\u5316",
2049
+ refineHint: "\u7ED9\u6A21\u578B\u66F4\u591A\u6307\u5F15\uFF0C\u91CD\u65B0\u51FA\u4E00\u7248\u8BA1\u5212",
2050
+ revise: "\u6539\u5199",
2051
+ reviseHint: "\u5728\u6267\u884C\u524D\u5C31\u5730\u7F16\u8F91\u8BA1\u5212\uFF08\u8DF3\u8FC7 / \u91CD\u6392\u6B65\u9AA4\uFF09",
2052
+ reject: "\u9A73\u56DE",
2053
+ rejectHint: "\u4E22\u5F03\uFF0C\u8BA9\u6A21\u578B\u4ECE\u5934\u518D\u6765"
2054
+ },
2055
+ refineFooter: "\u23CE \u53D1\u9001 \xB7 esc \u8FD4\u56DE\u9009\u9879",
2056
+ refineQuestionsHeading: "\u56DE\u7B54\u4EE5\u4E0B\u95EE\u9898\uFF0C\u6216\u76F4\u63A5\u8BF4\u660E\u4F60\u60F3\u8981\u7684\u4FEE\u6539\uFF1A",
2057
+ modes: {
2058
+ approve: {
2059
+ title: "\u91C7\u7EB3 \u2014\u2014 \u8FD8\u6709\u8865\u5145\u6307\u793A\u5417\uFF1F",
2060
+ hint: "\u56DE\u7B54\u8BA1\u5212\u4E2D\u7684\u95EE\u9898\u3001\u8865\u5145\u7EA6\u675F\uFF0C\u6216\u76F4\u63A5\u56DE\u8F66\u6309\u73B0\u72B6\u91C7\u7EB3\u3002",
2061
+ blankHint: "\uFF08\u7559\u7A7A\u56DE\u8F66 = \u4E0D\u9644\u52A0\u6307\u793A\u76F4\u63A5\u91C7\u7EB3\u3002\uFF09"
2062
+ },
2063
+ refine: {
2064
+ title: "\u7EC6\u5316 \u2014\u2014 \u6A21\u578B\u5E94\u8BE5\u6539\u4EC0\u4E48\uFF1F",
2065
+ hint: "\u8BF4\u660E\u95EE\u9898\u5728\u54EA\u3001\u7F3A\u4EC0\u4E48\uFF0C\u6216\u8005\u56DE\u7B54\u8BA1\u5212\u63D0\u51FA\u7684\u7591\u95EE\u3002",
2066
+ blankHint: "\uFF08\u7559\u7A7A\u56DE\u8F66 = \u8BA9\u6A21\u578B\u5BF9\u6240\u6709\u5F85\u786E\u8BA4\u95EE\u9898\u9009\u7528\u5B89\u5168\u9ED8\u8BA4\u3002\uFF09"
2067
+ },
2068
+ reject: {
2069
+ title: "\u9A73\u56DE \u2014\u2014 \u544A\u8BC9\u6A21\u578B\u539F\u56E0\uFF08\u53EF\u9009\uFF09",
2070
+ hint: "\u8BF4\u660E\u6A21\u578B\u5BF9\u4F60\u7684\u76EE\u6807\u7406\u89E3\u9519\u5728\u54EA\u91CC\uFF0C\u6216\u4F60\u771F\u6B63\u60F3\u8981\u4EC0\u4E48\u3002",
2071
+ blankHint: "\uFF08\u7559\u7A7A\u56DE\u8F66 = \u4E0D\u89E3\u91CA\u76F4\u63A5\u53D6\u6D88\uFF1B\u6A21\u578B\u4F1A\u53CD\u8FC7\u6765\u95EE\u4F60\u60F3\u8981\u4EC0\u4E48\u3002\uFF09"
2072
+ },
2073
+ "checkpoint-revise": {
2074
+ title: "\u6539\u5199 \u2014\u2014 \u4E0B\u4E00\u6B65\u524D\u8981\u8C03\u6574\u4EC0\u4E48\uFF1F",
2075
+ hint: "\u8303\u56F4\u8C03\u6574\u3001\u8DF3\u8FC7\u6B65\u9AA4\u3001\u6362\u4E2A\u601D\u8DEF \u2014\u2014 \u6A21\u578B\u4F1A\u636E\u6B64\u4FEE\u6539\u5269\u4F59\u6B65\u9AA4\u3002",
2076
+ blankHint: "\uFF08\u7559\u7A7A\u56DE\u8F66 = \u6309\u5F53\u524D\u8BA1\u5212\u7EE7\u7EED\u3002\uFF09"
2077
+ },
2078
+ "choice-custom": {
2079
+ title: "\u81EA\u5B9A\u4E49\u56DE\u7B54 \u2014\u2014 \u60F3\u8BF4\u4EC0\u4E48\u90FD\u884C",
2080
+ hint: "\u81EA\u7531\u6587\u672C\u3002\u6A21\u578B\u4F1A\u539F\u6837\u8BFB\u53D6\u5E76\u7EE7\u7EED \u2014\u2014 \u4E0D\u5FC5\u5339\u914D\u5019\u9009\u9879\u3002",
2081
+ blankHint: "\uFF08\u7559\u7A7A\u56DE\u8F66 = \u8BA9\u6A21\u578B\u53CD\u8FC7\u6765\u95EE\u4F60\u60F3\u8981\u4EC0\u4E48\u3002\uFF09"
2082
+ }
2083
+ },
2084
+ checkpoint: {
2085
+ title: "\u68C0\u67E5\u70B9 \u2014\u2014 \u5F53\u524D\u6B65\u9AA4\u5DF2\u5B8C\u6210",
2086
+ continue: "\u7EE7\u7EED \u2014\u2014 \u6267\u884C\u4E0B\u4E00\u6B65",
2087
+ continueHint: "\u6A21\u578B\u4ECE\u4E0B\u4E00\u6B65\u7EE7\u7EED\u3002",
2088
+ revise: "\u8C03\u6574 \u2014\u2014 \u5728\u4E0B\u4E00\u6B65\u524D\u7ED9\u53CD\u9988",
2089
+ reviseHint: "\u5148\u6682\u505C\uFF0C\u8F93\u5165\u6307\u5F15\uFF1B\u6A21\u578B\u4F1A\u8C03\u6574\u5269\u4F59\u8BA1\u5212\u3002",
2090
+ stop: "\u505C\u6B62 \u2014\u2014 \u5728\u6B64\u7ED3\u675F\u8BA1\u5212",
2091
+ stopHint: "\u6A21\u578B\u603B\u7ED3\u5DF2\u5B8C\u6210\u7684\u5DE5\u4F5C\u5E76\u7ED3\u675F\u3002"
2092
+ },
2093
+ stepList: {
2094
+ counter: "{total} \u4E2A\u6B65\u9AA4",
2095
+ counterSingular: "{total} \u4E2A\u6B65\u9AA4",
2096
+ counterDone: "{done}/{total} \u5DF2\u5B8C\u6210\uFF08{pct}%\uFF09 \xB7 \u5171 {total} \u6B65",
2097
+ counterDoneSingular: "{done}/{total} \u5DF2\u5B8C\u6210\uFF08{pct}%\uFF09 \xB7 \u5171 {total} \u6B65"
2098
+ }
2099
+ },
1976
2100
  app: {
1977
2101
  walkCancelledRemaining: "\u25B8 \u6D4F\u89C8\u5DF2\u53D6\u6D88 \u2014 \u8FD8\u6709 {count} \u4E2A\u5F85\u5904\u7406\u7F16\u8F91\u5757\u3002",
1978
2102
  walkCancelled: "\u25B8 \u6D4F\u89C8\u5DF2\u53D6\u6D88\u3002",
@@ -2879,6 +3003,7 @@ var ToolRegistry = class {
2879
3003
  _planMode = false;
2880
3004
  _interceptor = null;
2881
3005
  _auditListener = null;
3006
+ _resultAugmenter = null;
2882
3007
  constructor(opts = {}) {
2883
3008
  this._autoFlatten = opts.autoFlatten !== false;
2884
3009
  }
@@ -2897,6 +3022,10 @@ var ToolRegistry = class {
2897
3022
  setAuditListener(fn) {
2898
3023
  this._auditListener = fn;
2899
3024
  }
3025
+ /** Final-stage post-processor; replaces previous augmenter when called twice. Pass null to clear. */
3026
+ setResultAugmenter(fn) {
3027
+ this._resultAugmenter = fn;
3028
+ }
2900
3029
  register(def) {
2901
3030
  if (!def.name) throw new Error("tool requires a name");
2902
3031
  const internal = { ...def };
@@ -2972,6 +3101,7 @@ var ToolRegistry = class {
2972
3101
  });
2973
3102
  }
2974
3103
  }
3104
+ let finalResult;
2975
3105
  try {
2976
3106
  try {
2977
3107
  this._auditListener?.({ name, args });
@@ -2989,19 +3119,26 @@ var ToolRegistry = class {
2989
3119
  if (opts.maxResultChars !== void 0) {
2990
3120
  clipped = truncateForModel(clipped, opts.maxResultChars);
2991
3121
  }
2992
- return clipped;
3122
+ finalResult = clipped;
2993
3123
  } catch (err) {
2994
3124
  const e = err;
2995
3125
  if (typeof e.toToolResult === "function") {
2996
3126
  try {
2997
- return JSON.stringify(e.toToolResult());
3127
+ finalResult = JSON.stringify(e.toToolResult());
2998
3128
  } catch {
3129
+ finalResult = JSON.stringify({ error: `${e.name}: ${e.message}` });
2999
3130
  }
3131
+ } else {
3132
+ finalResult = JSON.stringify({ error: `${e.name}: ${e.message}` });
3133
+ }
3134
+ }
3135
+ if (this._resultAugmenter) {
3136
+ try {
3137
+ return this._resultAugmenter(name, args, finalResult);
3138
+ } catch {
3000
3139
  }
3001
- return JSON.stringify({
3002
- error: `${e.name}: ${e.message}`
3003
- });
3004
3140
  }
3141
+ return finalResult;
3005
3142
  }
3006
3143
  };
3007
3144
  function isReadOnlyCall(tool, args) {
@@ -5762,6 +5899,10 @@ var SkillStore = class {
5762
5899
  }
5763
5900
  /** Scaffold a new skill stub at the chosen scope. Refuses to overwrite. */
5764
5901
  create(name, scope) {
5902
+ return this.createWithContent(name, scope, skillStubBody(name));
5903
+ }
5904
+ /** Like `create` but writes caller-supplied file contents instead of the stub — used by the scaffold tool. */
5905
+ createWithContent(name, scope, content) {
5765
5906
  if (!isValidSkillName(name)) {
5766
5907
  return { error: `invalid skill name: "${name}" \u2014 use letters, digits, _, -, .` };
5767
5908
  }
@@ -5776,7 +5917,7 @@ var SkillStore = class {
5776
5917
  }
5777
5918
  mkdirSync3(dirname4(flat), { recursive: true });
5778
5919
  try {
5779
- writeFileSync3(flat, skillStubBody(name), { encoding: "utf8", flag: "wx" });
5920
+ writeFileSync3(flat, content, { encoding: "utf8", flag: "wx" });
5780
5921
  } catch (err) {
5781
5922
  if (err.code === "EEXIST") {
5782
5923
  return { error: `skill "${name}" already exists at ${flat}` };
@@ -6643,11 +6784,14 @@ async function searchFiles(ctx, startAbs, args) {
6643
6784
  await walk2(startAbs);
6644
6785
  return matches.length === 0 ? "(no matches)" : matches.join("\n");
6645
6786
  }
6787
+ var MAX_HITS_PER_FILE = 30;
6788
+ var SUMMARY_MODE_TRIGGER_RATIO = 0.8;
6646
6789
  async function searchContent(ctx, startAbs, args) {
6647
6790
  throwIfAborted(args.signal);
6648
6791
  const caseSensitive = args.case_sensitive === true;
6649
6792
  const includeDeps = args.include_deps === true;
6650
6793
  const ctxLines = Math.max(0, Math.min(20, Math.floor(args.context ?? 0)));
6794
+ const summaryOnly = args.summary_only === true;
6651
6795
  let re = null;
6652
6796
  try {
6653
6797
  re = new RegExp(args.pattern, caseSensitive ? "" : "i");
@@ -6659,6 +6803,9 @@ async function searchContent(ctx, startAbs, args) {
6659
6803
  let totalBytes = 0;
6660
6804
  let scanned = 0;
6661
6805
  let truncated = false;
6806
+ let summaryMode = summaryOnly;
6807
+ let summaryNoticeEmitted = false;
6808
+ const fileHitCounts = /* @__PURE__ */ new Map();
6662
6809
  const pushLine = (out) => {
6663
6810
  if (totalBytes + out.length + 1 > ctx.maxListBytes) {
6664
6811
  matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
@@ -6669,6 +6816,18 @@ async function searchContent(ctx, startAbs, args) {
6669
6816
  totalBytes += out.length + 1;
6670
6817
  return true;
6671
6818
  };
6819
+ const maybeEnterSummaryMode = () => {
6820
+ if (summaryMode) return;
6821
+ if (totalBytes <= SUMMARY_MODE_TRIGGER_RATIO * ctx.maxListBytes) return;
6822
+ summaryMode = true;
6823
+ if (!summaryNoticeEmitted) {
6824
+ const pct2 = Math.round(totalBytes / ctx.maxListBytes * 100);
6825
+ pushLine(
6826
+ `[switching to summary mode \u2014 byte budget at ${pct2}%; remaining files will report match counts only]`
6827
+ );
6828
+ summaryNoticeEmitted = true;
6829
+ }
6830
+ };
6672
6831
  const walk2 = async (dir) => {
6673
6832
  if (truncated) return;
6674
6833
  throwIfAborted(args.signal);
@@ -6727,33 +6886,48 @@ async function searchContent(ctx, startAbs, args) {
6727
6886
  }
6728
6887
  scanned++;
6729
6888
  if (hits.length === 0) continue;
6889
+ fileHitCounts.set(rel, hits.length);
6890
+ if (summaryMode) {
6891
+ if (!pushLine(`${rel}: ${hits.length} match${hits.length === 1 ? "" : "es"}`)) return;
6892
+ continue;
6893
+ }
6894
+ const printable = Math.min(hits.length, MAX_HITS_PER_FILE);
6895
+ const omittedFromFile = hits.length - printable;
6896
+ const printableHits = hits.slice(0, printable);
6730
6897
  if (ctxLines === 0) {
6731
- for (const li of hits) {
6898
+ for (const li of printableHits) {
6732
6899
  if (truncated) return;
6733
6900
  const line = lines[li];
6734
6901
  const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
6735
6902
  if (!pushLine(`${rel}:${li + 1}: ${display}`)) return;
6736
6903
  }
6737
- continue;
6738
- }
6739
- const hitSet = new Set(hits);
6740
- let prevWindowEnd = -2;
6741
- for (const li of hits) {
6742
- if (truncated) return;
6743
- const winStart = Math.max(0, li - ctxLines);
6744
- const winEnd = Math.min(lines.length - 1, li + ctxLines);
6745
- if (winStart > prevWindowEnd + 1 && prevWindowEnd >= 0) {
6746
- if (!pushLine("--")) return;
6747
- }
6748
- const realStart = winStart > prevWindowEnd + 1 ? winStart : prevWindowEnd + 1;
6749
- for (let i = realStart; i <= winEnd; i++) {
6750
- const line = lines[i];
6751
- const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
6752
- const sep2 = hitSet.has(i) ? ":" : "-";
6753
- if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
6904
+ } else {
6905
+ const hitSet = new Set(printableHits);
6906
+ let prevWindowEnd = -2;
6907
+ for (const li of printableHits) {
6908
+ if (truncated) return;
6909
+ const winStart = Math.max(0, li - ctxLines);
6910
+ const winEnd = Math.min(lines.length - 1, li + ctxLines);
6911
+ if (winStart > prevWindowEnd + 1 && prevWindowEnd >= 0) {
6912
+ if (!pushLine("--")) return;
6913
+ }
6914
+ const realStart = winStart > prevWindowEnd + 1 ? winStart : prevWindowEnd + 1;
6915
+ for (let i = realStart; i <= winEnd; i++) {
6916
+ const line = lines[i];
6917
+ const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
6918
+ const sep2 = hitSet.has(i) ? ":" : "-";
6919
+ if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
6920
+ }
6921
+ prevWindowEnd = winEnd;
6754
6922
  }
6755
- prevWindowEnd = winEnd;
6756
6923
  }
6924
+ if (omittedFromFile > 0) {
6925
+ if (!pushLine(
6926
+ `[${rel}: ${omittedFromFile} more match${omittedFromFile === 1 ? "" : "es"} in this file \u2014 re-grep with a tighter pattern or use read_file to see them]`
6927
+ ))
6928
+ return;
6929
+ }
6930
+ maybeEnterSummaryMode();
6757
6931
  }
6758
6932
  };
6759
6933
  await walk2(startAbs);
@@ -6769,6 +6943,43 @@ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
6769
6943
  var DEFAULT_AUTO_PREVIEW_LINES = 200;
6770
6944
  var AUTO_PREVIEW_HEAD_LINES = 80;
6771
6945
  var AUTO_PREVIEW_TAIL_LINES = 40;
6946
+ var OUTLINE_MAX_ENTRIES = 30;
6947
+ var OUTLINE_TAIL_KEEP = 5;
6948
+ var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
6949
+ function extractTsExportOutline(lines) {
6950
+ const out = [];
6951
+ for (let i = 0; i < lines.length; i++) {
6952
+ const line = lines[i];
6953
+ if (!line.startsWith("export ")) continue;
6954
+ const m = TS_EXPORT_RE.exec(line);
6955
+ if (!m) continue;
6956
+ out.push({ line: i + 1, kind: m[1], name: m[2] });
6957
+ }
6958
+ return out;
6959
+ }
6960
+ function formatOutline(entries) {
6961
+ const total = entries.length;
6962
+ if (total === 0) return "";
6963
+ const lastEntry = entries[total - 1];
6964
+ const width = String(lastEntry.line).length;
6965
+ const fmt = (e) => ` L${String(e.line).padStart(width, " ")} export ${e.kind} ${e.name}`;
6966
+ const header = `[outline: ${total} top-level export${total === 1 ? "" : "s"}]`;
6967
+ if (total <= OUTLINE_MAX_ENTRIES) {
6968
+ return [header, ...entries.map(fmt)].join("\n");
6969
+ }
6970
+ const headCount = OUTLINE_MAX_ENTRIES - OUTLINE_TAIL_KEEP;
6971
+ const headEntries = entries.slice(0, headCount);
6972
+ const tailEntries = entries.slice(-OUTLINE_TAIL_KEEP);
6973
+ const omitted = total - OUTLINE_MAX_ENTRIES;
6974
+ const gapStart = headEntries[headEntries.length - 1].line;
6975
+ const gapEnd = tailEntries[0].line;
6976
+ return [
6977
+ header,
6978
+ ...headEntries.map(fmt),
6979
+ ` [\u2026 ${omitted} more export${omitted === 1 ? "" : "s"} between L${gapStart} and L${gapEnd} \u2026]`,
6980
+ ...tailEntries.map(fmt)
6981
+ ].join("\n");
6982
+ }
6772
6983
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
6773
6984
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
6774
6985
  function displayRel4(rootDir, full) {
@@ -6821,7 +7032,7 @@ function registerFilesystemTools(registry, opts) {
6821
7032
  - head: N \u2192 first N lines (imports, public API, small configs)
6822
7033
  - tail: N \u2192 last N lines (recently-added code, log tails)
6823
7034
  - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
6824
- 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 rather than dumping everything. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first, then read_file with a range around the hit \u2014 one scoped read beats three full-file reads.`,
7035
+ 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.`,
6825
7036
  readOnly: true,
6826
7037
  stormExempt: true,
6827
7038
  parameters: {
@@ -6889,14 +7100,19 @@ ${slice.join("\n")}`;
6889
7100
  const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
6890
7101
  const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
6891
7102
  const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
6892
- return [
7103
+ const outline = formatOutline(extractTsExportOutline(lines));
7104
+ const parts = [
6893
7105
  `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
6894
- head,
7106
+ head
7107
+ ];
7108
+ if (outline) parts.push("", outline);
7109
+ parts.push(
6895
7110
  `
6896
7111
  [\u2026 ${omitted} lines omitted \u2014 call read_file again with range:"A-B" (1-indexed) or head / tail to get the middle]
6897
7112
  `,
6898
7113
  tail
6899
- ].join("\n");
7114
+ );
7115
+ return parts.join("\n");
6900
7116
  }
6901
7117
  });
6902
7118
  registry.register({
@@ -7029,7 +7245,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
7029
7245
  registry.register({
7030
7246
  name: "search_content",
7031
7247
  parallelSafe: true,
7032
- description: "Recursively grep file CONTENTS for a substring or regex. This is the right tool for 'find all places that call X', 'where is Y referenced', 'what files contain Z'. Different from search_files (which matches FILE NAMES). Returns one match per line in 'path:line: text' format. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) and binary files by default.",
7248
+ description: "Recursively grep file CONTENTS for a substring or regex. This is the right tool for 'find all places that call X', 'where is Y referenced', 'what files contain Z'. Different from search_files (which matches FILE NAMES). Returns one match per line in 'path:line: text' format. Per-file hits are capped at 30 (a footer reports any extras); when the byte budget is mostly spent the remaining files switch to a 'rel: N matches' histogram so distribution stays visible instead of one popular file drowning the rest. Pass `summary_only:true` to skip line content entirely and get just the histogram. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) and binary files by default.",
7033
7249
  readOnly: true,
7034
7250
  parameters: {
7035
7251
  type: "object",
@@ -7057,6 +7273,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
7057
7273
  context: {
7058
7274
  type: "integer",
7059
7275
  description: "Lines of context to show around each match (both before and after). Default 0 (just the matching line). Capped at 20. Output uses ripgrep style: `:` after the line number on the matching line, `-` on context lines, `--` separating non-adjacent windows."
7276
+ },
7277
+ summary_only: {
7278
+ type: "boolean",
7279
+ description: "When true, skip line content and return one 'rel: N matches' line per matching file. Use for 'where does this exist at all' questions before drilling in with a targeted read_file."
7060
7280
  }
7061
7281
  },
7062
7282
  required: ["pattern"]
@@ -7881,6 +8101,10 @@ var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
7881
8101
  var DEFAULT_MAX_ITERS = 16;
7882
8102
  var MIN_MAX_ITERS = 1;
7883
8103
  var MAX_MAX_ITERS = 32;
8104
+ var BUDGET_WARN_THRESHOLD = 3;
8105
+ function budgetParagraph(maxToolIters) {
8106
+ 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.`;
8107
+ }
7884
8108
  var DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
7885
8109
  var DEFAULT_SUBAGENT_EFFORT = "high";
7886
8110
  var SUBAGENT_TOOL_NAME = "spawn_subagent";
@@ -7939,8 +8163,26 @@ async function spawnSubagent(opts) {
7939
8163
  new Set(opts.allowedTools),
7940
8164
  NEVER_INHERITED_TOOLS
7941
8165
  ) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
8166
+ let dispatchCount = 0;
8167
+ childTools.setResultAugmenter((_name, _args, result) => {
8168
+ dispatchCount++;
8169
+ const remaining = maxToolIters - dispatchCount;
8170
+ if (remaining <= 0) {
8171
+ return `${result}
8172
+
8173
+ [budget: 0 of ${maxToolIters} tool calls left \u2014 finalize NOW; the next tool call will be refused]`;
8174
+ }
8175
+ if (remaining <= BUDGET_WARN_THRESHOLD) {
8176
+ return `${result}
8177
+
8178
+ [budget: ${remaining} of ${maxToolIters} tool call${remaining === 1 ? "" : "s"} left \u2014 wrap up soon]`;
8179
+ }
8180
+ return result;
8181
+ });
7942
8182
  const childPrefix = new ImmutablePrefix({
7943
- system: opts.system,
8183
+ system: `${opts.system}
8184
+
8185
+ ${budgetParagraph(maxToolIters)}`,
7944
8186
  toolSpecs: childTools.specs()
7945
8187
  });
7946
8188
  const childLoop = new CacheFirstLoop({