reasonix 0.31.0 → 0.33.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 (122) hide show
  1. package/README.md +3 -7
  2. package/README.zh-CN.md +2 -6
  3. package/dashboard/dist/app.js +348 -80
  4. package/dashboard/dist/app.js.map +1 -1
  5. package/dist/cli/chat-EIFLHBZ6.js +39 -0
  6. package/dist/cli/chunk-2AWTGJ2C.js +110 -0
  7. package/dist/cli/chunk-2AWTGJ2C.js.map +1 -0
  8. package/dist/cli/chunk-3Q3C4W66.js +30 -0
  9. package/dist/cli/chunk-3Q3C4W66.js.map +1 -0
  10. package/dist/cli/chunk-4DCHFFEY.js +149 -0
  11. package/dist/cli/chunk-4DCHFFEY.js.map +1 -0
  12. package/dist/cli/chunk-5X7LZJDE.js +36 -0
  13. package/dist/cli/chunk-5X7LZJDE.js.map +1 -0
  14. package/dist/cli/chunk-6TMHAK5D.js +576 -0
  15. package/dist/cli/chunk-6TMHAK5D.js.map +1 -0
  16. package/dist/cli/chunk-APPB3ZPQ.js +43 -0
  17. package/dist/cli/chunk-APPB3ZPQ.js.map +1 -0
  18. package/dist/cli/chunk-BQNUJJN7.js +42 -0
  19. package/dist/cli/chunk-BQNUJJN7.js.map +1 -0
  20. package/dist/cli/chunk-CPOV2O73.js +39 -0
  21. package/dist/cli/chunk-CPOV2O73.js.map +1 -0
  22. package/dist/cli/chunk-D5DKXIP5.js +368 -0
  23. package/dist/cli/chunk-D5DKXIP5.js.map +1 -0
  24. package/dist/cli/chunk-DFP4YSVM.js +247 -0
  25. package/dist/cli/chunk-DFP4YSVM.js.map +1 -0
  26. package/dist/cli/chunk-DULSP7JH.js +410 -0
  27. package/dist/cli/chunk-DULSP7JH.js.map +1 -0
  28. package/dist/cli/chunk-FM57FNPJ.js +46 -0
  29. package/dist/cli/chunk-FM57FNPJ.js.map +1 -0
  30. package/dist/cli/chunk-FWGEHRB7.js +54 -0
  31. package/dist/cli/chunk-FWGEHRB7.js.map +1 -0
  32. package/dist/cli/chunk-FXGQ5NHE.js +513 -0
  33. package/dist/cli/chunk-FXGQ5NHE.js.map +1 -0
  34. package/dist/cli/chunk-G3XNWSFN.js +53 -0
  35. package/dist/cli/chunk-G3XNWSFN.js.map +1 -0
  36. package/dist/cli/chunk-I6YIAK6C.js +757 -0
  37. package/dist/cli/chunk-I6YIAK6C.js.map +1 -0
  38. package/dist/cli/chunk-J5VLP23S.js +94 -0
  39. package/dist/cli/chunk-J5VLP23S.js.map +1 -0
  40. package/dist/cli/chunk-KMWKGPFZ.js +303 -0
  41. package/dist/cli/chunk-KMWKGPFZ.js.map +1 -0
  42. package/dist/cli/chunk-LVQX5KGF.js +14934 -0
  43. package/dist/cli/chunk-LVQX5KGF.js.map +1 -0
  44. package/dist/cli/chunk-MHDNZXJJ.js +48 -0
  45. package/dist/cli/chunk-MHDNZXJJ.js.map +1 -0
  46. package/dist/cli/chunk-ORM6PK57.js +140 -0
  47. package/dist/cli/chunk-ORM6PK57.js.map +1 -0
  48. package/dist/cli/chunk-Q5GRLZJF.js +99 -0
  49. package/dist/cli/chunk-Q5GRLZJF.js.map +1 -0
  50. package/dist/cli/chunk-Q6YFXW7H.js +4986 -0
  51. package/dist/cli/chunk-Q6YFXW7H.js.map +1 -0
  52. package/dist/cli/chunk-QGE6AF76.js +1467 -0
  53. package/dist/cli/chunk-QGE6AF76.js.map +1 -0
  54. package/dist/cli/chunk-RFX7TYVV.js +28 -0
  55. package/dist/cli/chunk-RFX7TYVV.js.map +1 -0
  56. package/dist/cli/chunk-RZILUXUC.js +940 -0
  57. package/dist/cli/chunk-RZILUXUC.js.map +1 -0
  58. package/dist/cli/chunk-SDE5U32Z.js +535 -0
  59. package/dist/cli/chunk-SDE5U32Z.js.map +1 -0
  60. package/dist/cli/chunk-SOZE7V7V.js +340 -0
  61. package/dist/cli/chunk-SOZE7V7V.js.map +1 -0
  62. package/dist/cli/chunk-U3V2ZQ5J.js +479 -0
  63. package/dist/cli/chunk-U3V2ZQ5J.js.map +1 -0
  64. package/dist/cli/chunk-W4LDFAZ6.js +1544 -0
  65. package/dist/cli/chunk-W4LDFAZ6.js.map +1 -0
  66. package/dist/cli/chunk-WBDE4IRI.js +208 -0
  67. package/dist/cli/chunk-WBDE4IRI.js.map +1 -0
  68. package/dist/cli/chunk-XHQIK7B6.js +189 -0
  69. package/dist/cli/chunk-XHQIK7B6.js.map +1 -0
  70. package/dist/cli/chunk-XJLZ4HKU.js +307 -0
  71. package/dist/cli/chunk-XJLZ4HKU.js.map +1 -0
  72. package/dist/cli/chunk-ZPTSJGX5.js +88 -0
  73. package/dist/cli/chunk-ZPTSJGX5.js.map +1 -0
  74. package/dist/cli/chunk-ZTLZO42A.js +231 -0
  75. package/dist/cli/chunk-ZTLZO42A.js.map +1 -0
  76. package/dist/cli/code-F4KJOE3K.js +151 -0
  77. package/dist/cli/code-F4KJOE3K.js.map +1 -0
  78. package/dist/cli/commands-JWT2MWVH.js +352 -0
  79. package/dist/cli/commands-JWT2MWVH.js.map +1 -0
  80. package/dist/cli/commit-RPZBOZS2.js +288 -0
  81. package/dist/cli/commit-RPZBOZS2.js.map +1 -0
  82. package/dist/cli/diff-NTEHCSDW.js +145 -0
  83. package/dist/cli/diff-NTEHCSDW.js.map +1 -0
  84. package/dist/cli/doctor-3TGB2NZN.js +19 -0
  85. package/dist/cli/doctor-3TGB2NZN.js.map +1 -0
  86. package/dist/cli/events-P27CX7LN.js +338 -0
  87. package/dist/cli/events-P27CX7LN.js.map +1 -0
  88. package/dist/cli/index.js +83 -34028
  89. package/dist/cli/index.js.map +1 -1
  90. package/dist/cli/mcp-ARTNQ24O.js +266 -0
  91. package/dist/cli/mcp-ARTNQ24O.js.map +1 -0
  92. package/dist/cli/mcp-browse-HLO2ENDL.js +163 -0
  93. package/dist/cli/mcp-browse-HLO2ENDL.js.map +1 -0
  94. package/dist/cli/mcp-inspect-T2HBR22P.js +103 -0
  95. package/dist/cli/mcp-inspect-T2HBR22P.js.map +1 -0
  96. package/dist/cli/{prompt-XHICFAYN.js → prompt-V47QKSAR.js} +3 -2
  97. package/dist/cli/prompt-V47QKSAR.js.map +1 -0
  98. package/dist/cli/prune-sessions-ERL6B4G5.js +42 -0
  99. package/dist/cli/prune-sessions-ERL6B4G5.js.map +1 -0
  100. package/dist/cli/replay-TMJASRC4.js +273 -0
  101. package/dist/cli/replay-TMJASRC4.js.map +1 -0
  102. package/dist/cli/run-JMEOTQCG.js +215 -0
  103. package/dist/cli/run-JMEOTQCG.js.map +1 -0
  104. package/dist/cli/server-SYC3OVOP.js +2967 -0
  105. package/dist/cli/server-SYC3OVOP.js.map +1 -0
  106. package/dist/cli/sessions-MOJAALJI.js +102 -0
  107. package/dist/cli/sessions-MOJAALJI.js.map +1 -0
  108. package/dist/cli/setup-CCJZAWTY.js +404 -0
  109. package/dist/cli/setup-CCJZAWTY.js.map +1 -0
  110. package/dist/cli/stats-5RJCATCE.js +12 -0
  111. package/dist/cli/stats-5RJCATCE.js.map +1 -0
  112. package/dist/cli/update-4TJWRUIN.js +90 -0
  113. package/dist/cli/update-4TJWRUIN.js.map +1 -0
  114. package/dist/cli/version-3MYFE4G6.js +29 -0
  115. package/dist/cli/version-3MYFE4G6.js.map +1 -0
  116. package/dist/index.d.ts +49 -96
  117. package/dist/index.js +567 -759
  118. package/dist/index.js.map +1 -1
  119. package/package.json +1 -1
  120. package/dist/cli/chunk-VWFJNLIK.js +0 -1031
  121. package/dist/cli/chunk-VWFJNLIK.js.map +0 -1
  122. /package/dist/cli/{prompt-XHICFAYN.js.map → chat-EIFLHBZ6.js.map} +0 -0
package/dist/index.js CHANGED
@@ -294,165 +294,6 @@ var DeepSeekClient = class {
294
294
  }
295
295
  };
296
296
 
297
- // src/harvest.ts
298
- function emptyPlanState() {
299
- return { subgoals: [], hypotheses: [], uncertainties: [], rejectedPaths: [] };
300
- }
301
- function isPlanStateEmpty(s) {
302
- if (!s) return true;
303
- return s.subgoals.length === 0 && s.hypotheses.length === 0 && s.uncertainties.length === 0 && s.rejectedPaths.length === 0;
304
- }
305
- var SYSTEM_PROMPT = `You extract a typed plan state from a reasoning trace produced by another LLM.
306
- Output ONLY a JSON object. No markdown, no prose, no backticks.
307
-
308
- Schema:
309
- {
310
- "subgoals": string[], // concrete intermediate objectives the trace identifies
311
- "hypotheses": string[], // candidate approaches or assumptions being weighed
312
- "uncertainties": string[], // facts the trace flags as unclear / to verify
313
- "rejectedPaths": string[] // approaches the trace considered and then abandoned
314
- }
315
-
316
- Constraints:
317
- - Every field must be present. Use [] if not applicable.
318
- - Each array has at most {maxItems} items.
319
- - Each item is plain text, at most {maxItemLen} characters, no markdown.
320
- - Write in the same language as the trace (Chinese in \u2192 Chinese out, etc.).
321
- - Do not quote back the trace; write short, specific phrases.`;
322
- async function harvest(reasoningContent, client, options = {}, signal) {
323
- if (!client || !reasoningContent) return emptyPlanState();
324
- if (signal?.aborted) return emptyPlanState();
325
- const minLen = options.minReasoningLen ?? 40;
326
- const trimmed = reasoningContent.trim();
327
- if (trimmed.length < minLen) return emptyPlanState();
328
- const model = options.model ?? "deepseek-v4-flash";
329
- const maxItems = options.maxItems ?? 5;
330
- const maxItemLen = options.maxItemLen ?? 80;
331
- const system = SYSTEM_PROMPT.replace("{maxItems}", String(maxItems)).replace(
332
- "{maxItemLen}",
333
- String(maxItemLen)
334
- );
335
- try {
336
- const resp = await client.chat({
337
- model,
338
- messages: [
339
- { role: "system", content: system },
340
- { role: "user", content: trimmed }
341
- ],
342
- responseFormat: { type: "json_object" },
343
- temperature: 0,
344
- maxTokens: 600,
345
- // Pin mode + effort so a future default-model swap (e.g. someone
346
- // sets `options.model = "deepseek-v4-pro"`) can't accidentally
347
- // turn this micro-extraction into a multi-thousand-reasoning-
348
- // token call. DeepSeek ignores these on non-thinking models, so
349
- // the request stays valid regardless of the chosen model.
350
- thinking: "disabled",
351
- reasoningEffort: "high",
352
- signal
353
- });
354
- return parsePlanState(resp.content, maxItems, maxItemLen);
355
- } catch {
356
- return emptyPlanState();
357
- }
358
- }
359
- function parsePlanState(raw, maxItems, maxItemLen) {
360
- const text = (raw ?? "").trim();
361
- if (!text) return emptyPlanState();
362
- let parsed;
363
- try {
364
- parsed = JSON.parse(text);
365
- } catch {
366
- const match = text.match(/\{[\s\S]*\}/);
367
- if (!match) return emptyPlanState();
368
- try {
369
- parsed = JSON.parse(match[0]);
370
- } catch {
371
- return emptyPlanState();
372
- }
373
- }
374
- if (!parsed || typeof parsed !== "object") return emptyPlanState();
375
- const obj = parsed;
376
- return {
377
- subgoals: sanitizeArray(obj.subgoals, maxItems, maxItemLen),
378
- hypotheses: sanitizeArray(obj.hypotheses, maxItems, maxItemLen),
379
- uncertainties: sanitizeArray(obj.uncertainties, maxItems, maxItemLen),
380
- rejectedPaths: sanitizeArray(obj.rejectedPaths ?? obj.rejected_paths, maxItems, maxItemLen)
381
- };
382
- }
383
- function sanitizeArray(raw, maxItems, maxItemLen) {
384
- if (!Array.isArray(raw)) return [];
385
- const out = [];
386
- for (const item of raw) {
387
- if (out.length >= maxItems) break;
388
- if (typeof item !== "string") continue;
389
- const cleaned = item.trim().replace(/\s+/g, " ");
390
- if (!cleaned) continue;
391
- out.push(cleaned.length <= maxItemLen ? cleaned : `${cleaned.slice(0, maxItemLen - 1)}\u2026`);
392
- }
393
- return out;
394
- }
395
-
396
- // src/consistency.ts
397
- var defaultSelector = (samples) => {
398
- if (samples.length === 0) throw new Error("defaultSelector: samples is empty");
399
- return samples.slice().sort((a, b) => {
400
- const uDiff = a.planState.uncertainties.length - b.planState.uncertainties.length;
401
- if (uDiff !== 0) return uDiff;
402
- const aLen = a.response.content?.length ?? 0;
403
- const bLen = b.response.content?.length ?? 0;
404
- return aLen - bLen;
405
- })[0];
406
- };
407
- async function runBranches(client, request, opts = {}) {
408
- const budget = Math.max(1, opts.budget ?? 1);
409
- const temperatures = resolveTemperatures(budget, opts.temperatures);
410
- const selector = opts.selector ?? defaultSelector;
411
- const samples = await Promise.all(
412
- temperatures.map(async (temperature, index) => {
413
- const response = await client.chat({ ...request, temperature });
414
- const planState = await harvest(response.reasoningContent, client, opts.harvestOptions);
415
- const sample = { index, temperature, response, planState };
416
- try {
417
- opts.onSampleDone?.(sample);
418
- } catch {
419
- }
420
- return sample;
421
- })
422
- );
423
- return { chosen: selector(samples), samples };
424
- }
425
- function aggregateBranchUsage(samples) {
426
- let promptTokens = 0;
427
- let completionTokens = 0;
428
- let totalTokens = 0;
429
- let promptCacheHitTokens = 0;
430
- let promptCacheMissTokens = 0;
431
- for (const s of samples) {
432
- promptTokens += s.response.usage.promptTokens;
433
- completionTokens += s.response.usage.completionTokens;
434
- totalTokens += s.response.usage.totalTokens;
435
- promptCacheHitTokens += s.response.usage.promptCacheHitTokens;
436
- promptCacheMissTokens += s.response.usage.promptCacheMissTokens;
437
- }
438
- return {
439
- promptTokens,
440
- completionTokens,
441
- totalTokens,
442
- promptCacheHitTokens,
443
- promptCacheMissTokens
444
- };
445
- }
446
- function resolveTemperatures(budget, custom) {
447
- if (custom && custom.length >= budget) return [...custom.slice(0, budget)];
448
- if (budget === 1) return [0];
449
- const out = [];
450
- for (let i = 0; i < budget; i++) {
451
- out.push(Number((i / (budget - 1)).toFixed(2)));
452
- }
453
- return out;
454
- }
455
-
456
297
  // src/core/pause-gate.ts
457
298
  var PauseGate = class {
458
299
  _nextId = 0;
@@ -775,21 +616,16 @@ var EN = {
775
616
  resumeHint: "force-resume the named session (even if idle)",
776
617
  newHint: "force a fresh session (ignore --session / --continue)",
777
618
  transcriptHint: "path to write the JSONL transcript",
778
- harvestHint: "opt into Pillar-2 plan-state extraction (costs +1 flash call per turn)",
779
619
  budgetHint: "session USD cap \u2014 warns at 80%, refuses next turn at 100%",
780
620
  modelIdHint: "DeepSeek model id (e.g. deepseek-v4-flash)",
781
621
  systemPromptHint: "override the default system prompt",
782
622
  presetHint: "model bundle \u2014 auto|flash|pro",
783
- harvestOptInHint: "opt into Pillar-2 plan-state extraction",
784
- branchHint: "run N parallel samples per turn (N>=2, manual only)",
785
623
  sessionNameHint: "session name (default: 'default')",
786
624
  ephemeralHint: "disable session persistence for this run",
787
625
  mcpSpecHint: "MCP server spec (repeatable)",
788
626
  mcpPrefixHint: "prefix MCP tool names with this string",
789
627
  noConfigHint: "ignore ~/.reasonix/config.json for this run",
790
628
  presetHintShort: "model bundle \u2014 auto|flash|pro",
791
- harvestHintShort: "Pillar-2 plan-state extraction",
792
- branchHintShort: "parallel samples per turn (N>=2)",
793
629
  budgetHintShort: "session USD cap",
794
630
  transcriptHintShort: "JSONL transcript path",
795
631
  mcpSpecHintShort: "MCP server spec (repeatable)",
@@ -847,12 +683,6 @@ var EN = {
847
683
  success: "Language switched to English.",
848
684
  unsupported: "Unsupported language code: {code}. Supported: {supported}."
849
685
  },
850
- harvest: { description: "toggle Pillar-2 plan-state extraction", argsHint: "[on|off]" },
851
- branch: { description: "run N parallel samples per turn (N>=2)", argsHint: "<N|off>" },
852
- effort: {
853
- description: "reasoning_effort cap \u2014 max is default (agent-class), high is cheaper/faster",
854
- argsHint: "<high|max>"
855
- },
856
686
  pro: {
857
687
  description: "arm v4-pro for the NEXT turn only (one-shot \xB7 auto-disarms after turn)",
858
688
  argsHint: "[off]"
@@ -870,7 +700,6 @@ var EN = {
870
700
  description: "browse + fetch MCP prompts (no arg \u2192 list names; <name> \u2192 render prompt)",
871
701
  argsHint: "[name]"
872
702
  },
873
- tool: { description: "dump full output of the Nth tool call (1=latest)", argsHint: "[N]" },
874
703
  memory: {
875
704
  description: "show / manage pinned memory (REASONIX.md + ~/.reasonix/memory)",
876
705
  argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]"
@@ -900,7 +729,6 @@ var EN = {
900
729
  argsHint: "[text]"
901
730
  },
902
731
  doctor: { description: "health check (api / config / api-reach / index / hooks / project)" },
903
- think: { description: "dump the last turn's full R1 reasoning (reasoner only)" },
904
732
  context: { description: "show context-window breakdown (system / tools / log / input)" },
905
733
  retry: { description: "truncate & resend your last message (fresh sample)" },
906
734
  compact: {
@@ -914,12 +742,6 @@ var EN = {
914
742
  argsHint: "[N]"
915
743
  },
916
744
  sessions: { description: "list saved sessions (current marked with \u25B8)" },
917
- rename: { description: "rename the current session on disk", argsHint: "<new-name>" },
918
- resume: {
919
- description: "show the launch command to resume a saved session",
920
- argsHint: "<name>"
921
- },
922
- forget: { description: "delete the current session from disk" },
923
745
  setup: { description: "reminds you to exit and run `reasonix setup`" },
924
746
  semantic: {
925
747
  description: "show semantic_search status \u2014 built? Ollama installed? how to enable"
@@ -965,9 +787,6 @@ var EN = {
965
787
  description: "toggle read-only plan mode (writes bounced until submit_plan + approval)",
966
788
  argsHint: "[on|off]"
967
789
  },
968
- "apply-plan": {
969
- description: "force-approve a pending / in-text plan (fallback if picker was missed)"
970
- },
971
790
  mode: {
972
791
  description: "edit-gate: review (queue) \xB7 auto (apply+undo) \xB7 yolo (apply+auto-shell). Shift+Tab cycles.",
973
792
  argsHint: "[review|auto|yolo]"
@@ -1111,49 +930,8 @@ var EN = {
1111
930
  },
1112
931
  handlers: {
1113
932
  basic: {
1114
- clearInfo: "\u25B8 terminal cleared (viewport + scrollback). Context (message log) is intact \u2014 next turn still sees everything. Use /new to start fresh, or /forget to delete the session entirely.",
1115
933
  newInfo: "\u25B8 new conversation \u2014 dropped {count} message(s) from context. Same session, fresh slate.",
1116
934
  helpTitle: "Commands:",
1117
- helpHelp: " /help this message",
1118
- helpKeys: " /keys keyboard shortcuts + prompt prefixes (!, @, /)",
1119
- helpStatus: " /status show current settings",
1120
- helpPreset: " /preset <auto|flash|pro> model bundle \u2014 see below",
1121
- helpModel: " /model <id> deepseek-v4-flash or deepseek-v4-pro",
1122
- helpPro: " /pro [off] arm v4-pro for NEXT turn only (one-shot, auto-disarms)",
1123
- helpHarvest: " /harvest [on|off] Pillar 2: structured plan-state extraction (OPT-IN \u2014 costs extra)",
1124
- helpBranch: " /branch <N|off> run N parallel samples (N>=2) \u2014 MANUAL ONLY, N\xD7 cost",
1125
- helpEffort: " /effort <high|max> reasoning_effort cap (max=full thinking, high=cheaper/faster)",
1126
- helpMcp: " /mcp list MCP servers + tools attached to this session",
1127
- helpResource: " /resource [uri] browse + read MCP resources (no arg \u2192 list URIs; <uri> \u2192 fetch)",
1128
- helpPrompt: " /prompt [name] browse + fetch MCP prompts (no arg \u2192 list names; <name> \u2192 render)",
1129
- helpCompact: " /compact fold older turns into a summary (cache-safe; auto-fires at 50% ctx)",
1130
- helpThink: " /think dump the most recent turn's full R1 reasoning (reasoner only)",
1131
- helpTool: " /tool [N] list tool calls (or dump full output of #N, 1=most recent)",
1132
- helpCost: " /cost [text] bare \u2192 last turn's spend; with text \u2192 estimate cost of sending it next",
1133
- helpMemory: " /memory [sub] show pinned memory (REASONIX.md + ~/.reasonix/memory).",
1134
- helpMemorySub: " subs: list | show <name> | forget <name> | clear <scope> confirm",
1135
- helpSkill: " /skill [sub] list / run user skills (project/.reasonix/skills + ~/.reasonix/skills).",
1136
- helpSkillSub: " subs: list | show <name> | <name> [args] (injects skill body as user turn)",
1137
- helpRetry: " /retry truncate & resend your last message (fresh sample from the model)",
1138
- helpApply: " /apply [N|1,3|1-4] (code mode) commit pending edit blocks (no arg \u2192 all; index \u2192 subset)",
1139
- helpDiscard: " /discard [N|1,3|1-4] (code mode) drop pending edits (no arg \u2192 all; index \u2192 subset)",
1140
- helpWalk: " /walk (code mode) step through pending edits one block at a time (y/n per block, a apply rest, A flip AUTO)",
1141
- helpUndo: " /undo (code mode) roll back the latest non-undone edit batch",
1142
- helpHistory: " /history (code mode) list every edit batch this session",
1143
- helpShow: " /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
1144
- helpCommit: ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
1145
- helpPlan: " /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
1146
- helpApplyPlan: " /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
1147
- helpMode: " /mode [review|auto|yolo] (code mode) review = queue \xB7 auto = apply+undo banner \xB7 yolo = apply+auto-shell. Shift+Tab cycles all three.",
1148
- helpJobs: " /jobs (code mode) list background processes (run_background) \u2014 running and exited",
1149
- helpKill: " /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
1150
- helpLogs: " /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
1151
- helpSessions: " /sessions list saved sessions (current is marked with \u25B8)",
1152
- helpForget: " /forget delete the current session from disk",
1153
- helpNew: " /new start fresh: drop all context + clear scrollback",
1154
- helpClear: " /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
1155
- helpLoop: " /loop <interval> <prompt> auto-resubmit <prompt> every <interval> (5s..6h). /loop stop \xB7 type anything to cancel.",
1156
- helpExit: " /exit quit (aliases: /quit, /q)",
1157
935
  helpShellTitle: "Shell shortcut:",
1158
936
  helpShell: " !<cmd> run <cmd> in the sandbox root; output goes into",
1159
937
  helpShellDetail: " the conversation so the model sees it next turn.",
@@ -1180,46 +958,6 @@ var EN = {
1180
958
  helpSessionsTitle: "Sessions (auto-enabled by default, named 'default'):",
1181
959
  helpSessionCustom: " reasonix chat --session <name> use a different named session",
1182
960
  helpSessionNone: " reasonix chat --no-session disable persistence for this run",
1183
- helpLimitationTitle: "Known limitation:",
1184
- helpLimitation1: " Resizing the terminal mid-session may stack ghost header frames in",
1185
- helpLimitation2: " scrollback (Ink library's live-region clear doesn't account for line",
1186
- helpLimitation3: " re-wrapping at the new width). Scroll-up history is unaffected; the",
1187
- helpLimitation4: " artifact is purely visual and clears the next time you /clear.",
1188
- keysTitle: "Keyboard & prompt shortcuts:",
1189
- keysEnter: " Enter submit the current prompt",
1190
- keysNewline: " Shift+Enter / Ctrl+J insert a newline (multi-line prompt)",
1191
- keysContinue: " \\<Enter> bash-style line continuation",
1192
- keysArrow: " \u2190 \u2192 \u2191 \u2193 move cursor / recall history at buffer boundary",
1193
- keysPage: " PageUp / PageDown jump to top / bottom of the WHOLE buffer (handy after a big paste)",
1194
- keysHomeEnd: " Ctrl+A / Ctrl+E jump to start / end of the CURRENT line",
1195
- keysClearLine: " Ctrl+U clear the entire input buffer",
1196
- keysDeleteWord: " Ctrl+W delete the word before the cursor",
1197
- keysBackspace: " Backspace delete left; Delete delete under cursor",
1198
- keysEsc: " Esc abort the in-flight turn",
1199
- keysEditYn: " y / n accept / reject pending edits (code mode)",
1200
- keysEditTab: " Shift+Tab cycle edit gate: review \u2194 AUTO (code mode, persists to config)",
1201
- keysEditUndo: " u undo the latest non-undone edit batch (session-wide, not just banner)",
1202
- keysPromptTitle: "Prompt prefixes:",
1203
- keysSlash: " /<name> slash command; Tab/Enter picks from the suggestion list",
1204
- keysAtFile: " @<path> inline a file under [Referenced files] (code mode).",
1205
- keysAtFilePicker: " Trailing `@\u2026` opens a file picker; \u2191/\u2193 navigate, Tab/Enter pick.",
1206
- keysAtUrl: " @https://... fetch the URL, strip HTML, inline under [Referenced URLs].",
1207
- keysAtUrlCache: " Cached per session \u2014 same URL twice fetches once.",
1208
- keysBang: " !<cmd> run <cmd> as shell in the sandbox root; output goes into context",
1209
- keysBangDetail: " so the model sees it next turn. No allowlist gate.",
1210
- keysHash: " #<note> append <note> to <project>/REASONIX.md (committable, team-shared).",
1211
- keysHashGlobal: " #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
1212
- keysHashBoth: " Both pin into the immutable prefix every future session.",
1213
- keysHashEscape: " Use `\\#literal` if you actually want a `#` heading sent to the model.",
1214
- keysPickersTitle: "Pickers (slash + @-mention):",
1215
- keysPickerNav: " \u2191 / \u2193 navigate the suggestion list",
1216
- keysPickerTab: " Tab insert the highlighted item without submitting",
1217
- keysPickerEnter: " Enter insert and (slash) run it, (@) keep editing",
1218
- keysMcpTitle: "MCP exploration:",
1219
- keysMcpServers: " /mcp servers + tool/resource/prompt counts",
1220
- keysMcpResource: " /resource [uri] browse & read resources exposed by your MCP servers",
1221
- keysMcpPrompt: " /prompt [name] browse & fetch prompts exposed by your MCP servers",
1222
- keysUseful: "Useful slashes: /help \xB7 /context \xB7 /stats \xB7 /compact \xB7 /new \xB7 /exit",
1223
961
  retryNone: "nothing to retry \u2014 no prior user message in this session's log.",
1224
962
  retryInfo: '\u25B8 retrying: "{preview}"',
1225
963
  loopTuiOnly: "/loop is only available in the interactive TUI (not in run/replay).",
@@ -1269,9 +1007,6 @@ var EN = {
1269
1007
  planCodeOnly: "/plan is only available inside `reasonix code` \u2014 chat mode doesn't gate tool writes.",
1270
1008
  planOn: "\u25B8 plan mode ON \u2014 write tools are gated; the model MUST call `submit_plan` before anything executes. (The model can also call submit_plan on its own for big tasks even when plan mode is off \u2014 this toggle is the stronger, explicit constraint.) Type /plan off to leave.",
1271
1009
  planOff: "\u25B8 plan mode OFF \u2014 write tools are live again. Model can still propose plans autonomously for large tasks.",
1272
- applyPlanCodeOnly: "/apply-plan is only available inside `reasonix code`.",
1273
- applyPlanInfo: "\u25B8 plan approved \u2014 implementing",
1274
- applyPlanResubmit: "The plan above has been approved. Implement it now. You are out of plan mode \u2014 use edit_file / write_file / run_command as needed. Stick to the plan unless you discover a concrete reason to deviate; if you do, tell me and wait for a response before making that deviation.",
1275
1010
  modeCodeOnly: "/mode is only available inside `reasonix code`.",
1276
1011
  modeUsage: "usage: /mode <review|auto|yolo> (Shift+Tab also cycles)",
1277
1012
  modeYolo: "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run with no prompt. /undo still rolls back edits. Use carefully.",
@@ -1297,31 +1032,20 @@ var EN = {
1297
1032
  restoreInfo: '\u25B8 restored "{name}" ({id}) from {when}',
1298
1033
  restoreWrote: " \xB7 wrote back {count} file{s}",
1299
1034
  restoreRemoved: " \xB7 removed {count} file{s} (didn't exist at checkpoint time)",
1300
- restoreSkipped: " \u2717 {count} file{s} skipped:"
1035
+ restoreSkipped: " \u2717 {count} file{s} skipped:",
1036
+ cwdCodeOnly: "/cwd is only available inside `reasonix code`.",
1037
+ cwdUsage: "usage: /cwd <path> (current root: {current}). Re-points filesystem / shell / memory tools to <path>.",
1038
+ cwdUsageNoCurrent: "usage: /cwd <path> re-points the workspace root to <path>."
1301
1039
  },
1302
1040
  model: {
1303
1041
  modelHint: "try deepseek-v4-flash or deepseek-v4-pro \u2014 run /models to fetch the live list",
1304
1042
  modelUsage: "usage: /model <id> ({hint})",
1305
1043
  modelNotInCatalog: "model \u2192 {id} (\u26A0 not in the fetched catalog: {list}. If this is wrong the next call will 400 \u2014 run /models to refresh.)",
1306
1044
  modelSet: "model \u2192 {id}",
1307
- modelsFetching: "fetching /models from DeepSeek\u2026 run /models again in a moment. If it stays empty, your API key may lack permission or the network is blocked.",
1308
- modelsEmpty: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com.",
1309
- modelsHeader: "Available models (DeepSeek /models \xB7 {count} total):",
1310
- modelsCurrent: "\u25B8 {id} (current)",
1311
- modelsSwitch: "Switch with: /model <id>",
1312
- harvestOn: "harvest \u2192 on (Pillar-2 plan-state extraction \xB7 +1 cheap flash call per turn \xB7 opt-in only; no preset turns it on)",
1313
- harvestOff: "harvest \u2192 off",
1314
1045
  presetAuto: "preset \u2192 auto (v4-flash \u2192 v4-pro on hard turns \xB7 default)",
1315
1046
  presetFlash: "preset \u2192 flash (v4-flash always \xB7 cheapest \xB7 /pro still bumps one turn)",
1316
1047
  presetPro: "preset \u2192 pro (v4-pro always \xB7 ~3\xD7 flash \xB7 for hard multi-turn work)",
1317
1048
  presetUsage: "usage: /preset <auto|flash|pro>",
1318
- branchOff: "branch \u2192 off",
1319
- branchUsage: "usage: /branch <N> (N>=2, or 'off')",
1320
- branchCapped: "branch budget capped at 8 to prevent runaway cost",
1321
- branchSet: "branch \u2192 {n} (runs {n} parallel samples per turn \xB7 {n}\xD7 per-turn cost \xB7 streaming disabled \xB7 manual only, no preset enables branching)",
1322
- effortStatus: "reasoning_effort \u2192 {effort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)",
1323
- effortUsage: "usage: /effort <high|max>",
1324
- effortSet: "reasoning_effort \u2192 {effort} (persisted)",
1325
1049
  proNothingArmed: "nothing armed \u2014 /pro with no args will arm pro for your next turn",
1326
1050
  proDisarmed: "\u25B8 /pro disarmed \u2014 next turn falls back to the current preset",
1327
1051
  proUsage: "usage: /pro arm pro for the next turn (one-shot, auto-disarms after)\n /pro off cancel armed state before the next turn",
@@ -1333,18 +1057,6 @@ var EN = {
1333
1057
  budgetExhausted: "\u25B2 budget \u2192 ${cap} but already spent ${spent}. Next turn will be refused \u2014 bump the cap higher to keep going, or end the session.",
1334
1058
  budgetSet: "budget \u2192 ${cap} (so far: ${spent} \xB7 warns at 80%, refuses next turn at 100% \xB7 /budget off to clear)"
1335
1059
  },
1336
- sessions: {
1337
- forgetNoSession: "not in a session \u2014 nothing to forget",
1338
- forgetInfo: '\u25B8 deleted session "{name}" \u2014 current screen still shows the conversation, but next launch starts fresh',
1339
- forgetFailed: 'could not delete session "{name}" (already gone?)',
1340
- renameUsage: "usage: /rename <new-name>",
1341
- renameNoSession: "not in a session \u2014 nothing to rename",
1342
- renameFailed: 'could not rename \u2014 "{name}" already exists or sanitises to the same id as the current session',
1343
- renameInfo: '\u25B8 renamed session \u2192 "{name}". Restart the TUI to pick it up under its new name.',
1344
- resumeUsage: "usage: /resume <session-name> \u2014 list with /sessions",
1345
- resumeNotFound: 'no session named "{name}" \u2014 list with /sessions',
1346
- resumeInfo: '\u25B8 to resume "{name}", quit and run: reasonix chat --session {name}\n (mid-session swap requires a restart so the message log can rewind cleanly)'
1347
- },
1348
1060
  permissions: {
1349
1061
  mutateCodeOnly: "/permissions add / remove / clear are only available inside `reasonix code` \u2014 they edit the project-scoped allowlist (`~/.reasonix/config.json` projects[<root>].shellAllowed).",
1350
1062
  addUsage: 'usage: /permissions add <prefix> (multi-token OK: /permissions add "git push origin")',
@@ -1386,13 +1098,6 @@ var EN = {
1386
1098
  starting: "\u25B8 starting dashboard server\u2026"
1387
1099
  },
1388
1100
  observability: {
1389
- thinkEmpty: "no reasoning cached. `/think` shows the full thinking-mode thought for the most recent turn \u2014 only thinking-mode models (deepseek-v4-flash / -v4-pro / -reasoner) produce it, and only once the turn completes.",
1390
- thinkInfo: "\u21B3 full thinking ({count} chars):",
1391
- toolEmpty: "no tool calls yet in this session. `/tool` lists them once the model has actually used a tool; `/tool N` dumps the full (untruncated) output of the Nth-most-recent.",
1392
- toolUsage: "usage: /tool [N] (no arg \u2192 list; N=1 \u2192 most recent result in full, N=2 \u2192 previous, \u2026)",
1393
- toolOob: "only {count} tool call(s) in history \u2014 asked for #{n}. Try /tool with no arg to see the list.",
1394
- toolNotFound: "could not read tool call #{n}",
1395
- toolInfo: "\u21B3 tool<{name}> #{n} ({chars} chars):",
1396
1101
  contextInfo: "context: ~{total} of {max} ({pct}%) \xB7 system {sys} \xB7 tools {tools} \xB7 log {log}",
1397
1102
  compactStarting: "\u25B8 folding older turns into a summary\u2026",
1398
1103
  compactNoop: "\u25B8 nothing to fold \u2014 log already small or recent turns alone exceed the budget.",
@@ -1406,7 +1111,7 @@ var EN = {
1406
1111
  costLikely: " likely ({pct}% session cache hit): {input} input + ~{output} output \u2248 {total}",
1407
1112
  costLikelyCold: " likely: matches worst case until cache fills (no completed turns yet)",
1408
1113
  statusModel: " model {model}",
1409
- statusFlags: " flags harvest={harvest} \xB7 branch={branch} \xB7 stream={stream} \xB7 effort={effort}",
1114
+ statusFlags: " flags stream={stream} \xB7 effort={effort}",
1410
1115
  statusCtx: " ctx {bar} {used}/{max} ({pct}%)",
1411
1116
  statusCtxNone: " ctx no turns yet",
1412
1117
  statusCost: " cost ${cost} \xB7 cache {bar} {pct}% \xB7 turns {turns}",
@@ -1508,10 +1213,6 @@ var EN = {
1508
1213
  existsPinned: " pinned into the system prompt every launch as-is.",
1509
1214
  info: "\u25B8 /init \u2014 model will scan the project and synthesize REASONIX.md.\n The result lands as a pending edit; review with /apply or /walk."
1510
1215
  },
1511
- semantic: {
1512
- codeOnly: "/semantic is only available inside `reasonix code` (needs a project root).",
1513
- checking: "\u25B8 checking semantic_search status\u2026"
1514
- },
1515
1216
  webSearchEngine: {
1516
1217
  currentEngine: "Current web search engine: {engine}",
1517
1218
  endpoint: "SearXNG endpoint: {url}",
@@ -1608,21 +1309,16 @@ var zhCN = {
1608
1309
  resumeHint: "\u5F3A\u5236\u6062\u590D\u6307\u5B9A\u4F1A\u8BDD\uFF08\u5373\u4F7F\u7A7A\u95F2\uFF09",
1609
1310
  newHint: "\u5F3A\u5236\u521B\u5EFA\u65B0\u4F1A\u8BDD\uFF08\u5FFD\u7565 --session / --continue\uFF09",
1610
1311
  transcriptHint: "JSONL \u8F6C\u5F55\u7A3F\u7684\u5199\u5165\u8DEF\u5F84",
1611
- harvestHint: "\u542F\u7528 Pillar-2 \u8BA1\u5212\u72B6\u6001\u63D0\u53D6\uFF08\u6BCF\u8F6E\u989D\u5916\u6D88\u8017 1 \u6B21 flash \u8C03\u7528\uFF09",
1612
1312
  budgetHint: "\u4F1A\u8BDD\u7F8E\u5143\u4E0A\u9650 \u2014 80% \u65F6\u8B66\u544A\uFF0C100% \u65F6\u62D2\u7EDD\u4E0B\u4E00\u8F6E",
1613
1313
  modelIdHint: "DeepSeek \u6A21\u578B ID\uFF08\u4F8B\u5982 deepseek-v4-flash\uFF09",
1614
1314
  systemPromptHint: "\u8986\u76D6\u9ED8\u8BA4\u7CFB\u7EDF\u63D0\u793A\u8BCD",
1615
1315
  presetHint: "\u6A21\u578B\u7EC4\u5408 \u2014 auto|flash|pro",
1616
- harvestOptInHint: "\u542F\u7528 Pillar-2 \u8BA1\u5212\u72B6\u6001\u63D0\u53D6",
1617
- branchHint: "\u6BCF\u8F6E\u8FD0\u884C N \u4E2A\u5E76\u884C\u91C7\u6837\uFF08N>=2\uFF0C\u4EC5\u624B\u52A8\uFF09",
1618
1316
  sessionNameHint: "\u4F1A\u8BDD\u540D\u79F0\uFF08\u9ED8\u8BA4\uFF1A'default'\uFF09",
1619
1317
  ephemeralHint: "\u7981\u7528\u672C\u6B21\u8FD0\u884C\u7684\u4F1A\u8BDD\u6301\u4E45\u5316",
1620
1318
  mcpSpecHint: "MCP \u670D\u52A1\u5668\u89C4\u683C\uFF08\u53EF\u91CD\u590D\uFF09",
1621
1319
  mcpPrefixHint: "\u7528\u6B64\u5B57\u7B26\u4E32\u4E3A MCP \u5DE5\u5177\u540D\u6DFB\u52A0\u524D\u7F00",
1622
1320
  noConfigHint: "\u672C\u6B21\u8FD0\u884C\u5FFD\u7565 ~/.reasonix/config.json",
1623
1321
  presetHintShort: "\u6A21\u578B\u7EC4\u5408 \u2014 auto|flash|pro",
1624
- harvestHintShort: "Pillar-2 \u8BA1\u5212\u72B6\u6001\u63D0\u53D6",
1625
- branchHintShort: "\u6BCF\u8F6E\u5E76\u884C\u91C7\u6837\u6570\uFF08N>=2\uFF09",
1626
1322
  budgetHintShort: "\u4F1A\u8BDD\u7F8E\u5143\u4E0A\u9650",
1627
1323
  transcriptHintShort: "JSONL \u8F6C\u5F55\u7A3F\u8DEF\u5F84",
1628
1324
  mcpSpecHintShort: "MCP \u670D\u52A1\u5668\u89C4\u683C\uFF08\u53EF\u91CD\u590D\uFF09",
@@ -1680,12 +1376,6 @@ var zhCN = {
1680
1376
  success: "\u8BED\u8A00\u5DF2\u5207\u6362\u4E3A\u7B80\u4F53\u4E2D\u6587\u3002",
1681
1377
  unsupported: "\u4E0D\u652F\u6301\u7684\u8BED\u8A00\u4EE3\u7801\uFF1A{code}\u3002\u652F\u6301\u7684\u8BED\u8A00\uFF1A{supported}\u3002"
1682
1378
  },
1683
- harvest: { description: "\u5207\u6362 Pillar-2 \u8BA1\u5212\u72B6\u6001\u63D0\u53D6", argsHint: "[on|off]" },
1684
- branch: { description: "\u6BCF\u8F6E\u8FD0\u884C N \u4E2A\u5E76\u884C\u91C7\u6837\uFF08N>=2\uFF09", argsHint: "<N|off>" },
1685
- effort: {
1686
- description: "reasoning_effort \u4E0A\u9650 \u2014 max \u4E3A\u9ED8\u8BA4\uFF08\u667A\u80FD\u4F53\u7EA7\uFF09\uFF0Chigh \u66F4\u4FBF\u5B9C/\u66F4\u5FEB",
1687
- argsHint: "<high|max>"
1688
- },
1689
1379
  pro: {
1690
1380
  description: "\u4EC5\u4E3A\u4E0B\u4E00\u8F6E\u542F\u7528 v4-pro\uFF08\u4E00\u6B21\u6027 \xB7 \u81EA\u52A8\u89E3\u9664\uFF09",
1691
1381
  argsHint: "[off]"
@@ -1703,7 +1393,6 @@ var zhCN = {
1703
1393
  description: "\u6D4F\u89C8 + \u83B7\u53D6 MCP \u63D0\u793A\uFF08\u65E0\u53C2\u6570 \u2192 \u5217\u51FA\u540D\u79F0\uFF1B<name> \u2192 \u6E32\u67D3\u63D0\u793A\uFF09",
1704
1394
  argsHint: "[name]"
1705
1395
  },
1706
- tool: { description: "\u8F6C\u50A8\u7B2C N \u4E2A\u5DE5\u5177\u8C03\u7528\u7684\u5B8C\u6574\u8F93\u51FA\uFF081=\u6700\u8FD1\uFF09", argsHint: "[N]" },
1707
1396
  memory: {
1708
1397
  description: "\u663E\u793A / \u7BA1\u7406\u56FA\u5B9A\u8BB0\u5FC6\uFF08REASONIX.md + ~/.reasonix/memory\uFF09",
1709
1398
  argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]"
@@ -1735,7 +1424,6 @@ var zhCN = {
1735
1424
  doctor: {
1736
1425
  description: "\u5065\u5EB7\u68C0\u67E5\uFF08api / config / api-reach / index / hooks / project\uFF09"
1737
1426
  },
1738
- think: { description: "\u8F6C\u50A8\u6700\u8FD1\u4E00\u8F6E\u7684\u5B8C\u6574 R1 \u63A8\u7406\uFF08\u4EC5\u63A8\u7406\u6A21\u578B\uFF09" },
1739
1427
  context: { description: "\u663E\u793A\u4E0A\u4E0B\u6587\u7A97\u53E3\u5206\u89E3\uFF08\u7CFB\u7EDF / \u5DE5\u5177 / \u65E5\u5FD7 / \u8F93\u5165\uFF09" },
1740
1428
  retry: { description: "\u622A\u65AD\u5E76\u91CD\u53D1\u60A8\u7684\u6700\u540E\u4E00\u6761\u6D88\u606F\uFF08\u91CD\u65B0\u91C7\u6837\uFF09" },
1741
1429
  compact: {
@@ -1749,12 +1437,6 @@ var zhCN = {
1749
1437
  argsHint: "[N]"
1750
1438
  },
1751
1439
  sessions: { description: "\u5217\u51FA\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD\uFF08\u5F53\u524D\u6807\u8BB0\u4E3A \u25B8\uFF09" },
1752
- rename: { description: "\u91CD\u547D\u540D\u78C1\u76D8\u4E0A\u7684\u5F53\u524D\u4F1A\u8BDD", argsHint: "<new-name>" },
1753
- resume: {
1754
- description: "\u663E\u793A\u6062\u590D\u5DF2\u4FDD\u5B58\u4F1A\u8BDD\u7684\u542F\u52A8\u547D\u4EE4",
1755
- argsHint: "<name>"
1756
- },
1757
- forget: { description: "\u4ECE\u78C1\u76D8\u5220\u9664\u5F53\u524D\u4F1A\u8BDD" },
1758
1440
  setup: { description: "\u63D0\u9192\u60A8\u9000\u51FA\u5E76\u8FD0\u884C `reasonix setup`" },
1759
1441
  semantic: {
1760
1442
  description: "\u663E\u793A semantic_search \u72B6\u6001 \u2014 \u5DF2\u6784\u5EFA\uFF1FOllama \u5DF2\u5B89\u88C5\uFF1F\u5982\u4F55\u542F\u7528"
@@ -1802,9 +1484,6 @@ var zhCN = {
1802
1484
  description: "\u5207\u6362\u53EA\u8BFB\u8BA1\u5212\u6A21\u5F0F\uFF08\u5199\u5165\u88AB\u5F39\u56DE\u76F4\u5230 submit_plan + \u5BA1\u6279\uFF09",
1803
1485
  argsHint: "[on|off]"
1804
1486
  },
1805
- "apply-plan": {
1806
- description: "\u5F3A\u5236\u6279\u51C6\u5F85\u5904\u7406 / \u6587\u672C\u4E2D\u7684\u8BA1\u5212\uFF08\u5982\u679C\u9519\u8FC7\u4E86\u9009\u62E9\u5668\u65F6\u7684\u56DE\u9000\uFF09"
1807
- },
1808
1487
  mode: {
1809
1488
  description: "\u7F16\u8F91\u95E8\u63A7\uFF1Areview\uFF08\u6392\u961F\uFF09\xB7 auto\uFF08\u5E94\u7528+\u64A4\u6D88\uFF09\xB7 yolo\uFF08\u5E94\u7528+\u81EA\u52A8 shell\uFF09\u3002Shift+Tab \u5FAA\u73AF\u3002",
1810
1489
  argsHint: "[review|auto|yolo]"
@@ -1948,49 +1627,8 @@ var zhCN = {
1948
1627
  },
1949
1628
  handlers: {
1950
1629
  basic: {
1951
- clearInfo: "\u25B8 \u7EC8\u7AEF\u5DF2\u6E05\u9664\uFF08\u89C6\u53E3 + \u6EDA\u52A8\u56DE\u653E\uFF09\u3002\u4E0A\u4E0B\u6587\uFF08\u6D88\u606F\u65E5\u5FD7\uFF09\u5B8C\u597D\u65E0\u635F \u2014 \u4E0B\u4E00\u8F6E\u4ECD\u80FD\u770B\u5230\u6240\u6709\u5185\u5BB9\u3002\u4F7F\u7528 /new \u5168\u65B0\u5F00\u59CB\uFF0C\u6216 /forget \u5220\u9664\u6574\u4E2A\u4F1A\u8BDD\u3002",
1952
1630
  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",
1953
1631
  helpTitle: "\u547D\u4EE4\uFF1A",
1954
- helpHelp: " /help \u663E\u793A\u6B64\u6D88\u606F",
1955
- helpKeys: " /keys \u952E\u76D8\u5FEB\u6377\u952E + \u63D0\u793A\u524D\u7F00 (!, @, /)",
1956
- helpStatus: " /status \u663E\u793A\u5F53\u524D\u8BBE\u7F6E",
1957
- helpPreset: " /preset <auto|flash|pro> \u6A21\u578B\u7EC4\u5408 \u2014 \u89C1\u4E0B\u6587",
1958
- helpModel: " /model <id> deepseek-v4-flash \u6216 deepseek-v4-pro",
1959
- helpPro: " /pro [off] \u4E3A\u4E0B\u4E00\u8F6E\u542F\u7528 v4-pro\uFF08\u4E00\u6B21\u6027\uFF0C\u81EA\u52A8\u89E3\u9664\uFF09",
1960
- helpHarvest: " /harvest [on|off] Pillar 2\uFF1A\u7ED3\u6784\u5316\u8BA1\u5212\u72B6\u6001\u63D0\u53D6\uFF08\u53EF\u9009 \u2014 \u989D\u5916\u6536\u8D39\uFF09",
1961
- helpBranch: " /branch <N|off> \u8FD0\u884C N \u4E2A\u5E76\u884C\u91C7\u6837\uFF08N>=2\uFF09\u2014 \u4EC5\u624B\u52A8\uFF0CN \u500D\u6210\u672C",
1962
- helpEffort: " /effort <high|max> reasoning_effort \u4E0A\u9650\uFF08max=\u5B8C\u6574\u601D\u8003\uFF0Chigh=\u66F4\u4FBF\u5B9C/\u66F4\u5FEB\uFF09",
1963
- helpMcp: " /mcp \u5217\u51FA\u9644\u52A0\u5230\u6B64\u4F1A\u8BDD\u7684 MCP \u670D\u52A1\u5668 + \u5DE5\u5177",
1964
- helpResource: " /resource [uri] \u6D4F\u89C8 + \u8BFB\u53D6 MCP \u8D44\u6E90\uFF08\u65E0\u53C2\u6570 \u2192 \u5217\u51FA URI\uFF1B<uri> \u2192 \u83B7\u53D6\uFF09",
1965
- helpPrompt: " /prompt [name] \u6D4F\u89C8 + \u83B7\u53D6 MCP \u63D0\u793A\uFF08\u65E0\u53C2\u6570 \u2192 \u5217\u51FA\u540D\u79F0\uFF1B<name> \u2192 \u6E32\u67D3\uFF09",
1966
- helpCompact: " /compact \u6298\u53E0\u65E7\u8F6E\u6B21\u4E3A\u6458\u8981\uFF08cache-safe\uFF0C50% \u81EA\u52A8\u89E6\u53D1\uFF09",
1967
- helpThink: " /think \u8F6C\u50A8\u6700\u8FD1\u4E00\u8F6E\u7684\u5B8C\u6574 R1 \u63A8\u7406\uFF08\u4EC5\u63A8\u7406\u6A21\u578B\uFF09",
1968
- helpTool: " /tool [N] \u5217\u51FA\u5DE5\u5177\u8C03\u7528\uFF08\u6216\u8F6C\u50A8\u7B2C N \u4E2A\u7684\u5B8C\u6574\u8F93\u51FA\uFF0C1=\u6700\u8FD1\uFF09",
1969
- helpCost: " /cost [text] \u7A7A \u2192 \u4E0A\u4E00\u8F6E\u82B1\u8D39\uFF1B\u5E26\u6587\u672C \u2192 \u4F30\u7B97\u53D1\u9001\u6210\u672C",
1970
- helpMemory: " /memory [sub] \u663E\u793A\u56FA\u5B9A\u8BB0\u5FC6\uFF08REASONIX.md + ~/.reasonix/memory\uFF09\u3002",
1971
- helpMemorySub: " \u5B50\u547D\u4EE4\uFF1Alist | show <name> | forget <name> | clear <scope> confirm",
1972
- helpSkill: " /skill [sub] \u5217\u51FA / \u8FD0\u884C\u7528\u6237\u6280\u80FD\uFF08project/.reasonix/skills + ~/.reasonix/skills\uFF09\u3002",
1973
- helpSkillSub: " \u5B50\u547D\u4EE4\uFF1Alist | show <name> | <name> [args]\uFF08\u5C06\u6280\u80FD\u4F53\u6CE8\u5165\u4E3A\u7528\u6237\u8F6E\u6B21\uFF09",
1974
- helpRetry: " /retry \u622A\u65AD\u5E76\u91CD\u53D1\u60A8\u7684\u6700\u540E\u4E00\u6761\u6D88\u606F\uFF08\u6A21\u578B\u91CD\u65B0\u91C7\u6837\uFF09",
1975
- helpApply: " /apply [N|1,3|1-4] \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u63D0\u4EA4\u5F85\u5904\u7406\u7684\u7F16\u8F91\u5757\uFF08\u65E0\u53C2\u6570 \u2192 \u5168\u90E8\uFF1B\u7D22\u5F15 \u2192 \u5B50\u96C6\uFF09",
1976
- helpDiscard: " /discard [N|1,3|1-4] \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u4E22\u5F03\u5F85\u5904\u7406\u7684\u7F16\u8F91\uFF08\u65E0\u53C2\u6570 \u2192 \u5168\u90E8\uFF1B\u7D22\u5F15 \u2192 \u5B50\u96C6\uFF09",
1977
- helpWalk: " /walk \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u9010\u5757\u9010\u6B65\u5904\u7406\u5F85\u5904\u7406\u7684\u7F16\u8F91\uFF08\u6BCF\u5757 y/n\uFF0Ca \u5E94\u7528\u5269\u4F59\uFF0CA \u5207\u6362 AUTO\uFF09",
1978
- helpUndo: " /undo \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u56DE\u6EDA\u6700\u8FD1\u672A\u64A4\u6D88\u7684\u7F16\u8F91\u6279\u5904\u7406",
1979
- helpHistory: " /history \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u5217\u51FA\u6B64\u4F1A\u8BDD\u7684\u6BCF\u4E2A\u7F16\u8F91\u6279\u5904\u7406",
1980
- helpShow: " /show [id] \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u8F6C\u50A8\u5B58\u50A8\u7684\u7F16\u8F91\u5DEE\u5F02\uFF08\u7701\u7565 id \u65F6\u4E3A\u6700\u65B0\uFF09",
1981
- helpCommit: ' /commit "msg" \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09git add -A && git commit -m "msg"',
1982
- helpPlan: " /plan [on|off] \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u5207\u6362\u53EA\u8BFB\u8BA1\u5212\u6A21\u5F0F\uFF1B\u5199\u5165\u9700\u7ECF submit_plan + \u5BA1\u6279",
1983
- helpApplyPlan: " /apply-plan \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u5F3A\u5236\u6279\u51C6\u5F85\u5904\u7406/\u6587\u672C\u4E2D\u7684\u8BA1\u5212\uFF08\u56DE\u9000\uFF09",
1984
- helpMode: " /mode [review|auto|yolo] \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09review = \u6392\u961F \xB7 auto = \u5E94\u7528+\u64A4\u6D88\u6A2A\u5E45 \xB7 yolo = \u5E94\u7528+\u81EA\u52A8 shell\u3002Shift+Tab \u5FAA\u73AF\u5207\u6362\u3002",
1985
- helpJobs: " /jobs \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u5217\u51FA\u540E\u53F0\u8FDB\u7A0B\uFF08run_background\uFF09\u2014 \u8FD0\u884C\u4E2D\u548C\u5DF2\u9000\u51FA",
1986
- helpKill: " /kill <id> \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u6309 ID \u505C\u6B62\u540E\u53F0\u4F5C\u4E1A\uFF08SIGTERM \u2192 SIGKILL\uFF09",
1987
- helpLogs: " /logs <id> [lines] \uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u8DDF\u8E2A\u540E\u53F0\u4F5C\u4E1A\u8F93\u51FA\uFF08\u9ED8\u8BA4\u6700\u540E 80 \u884C\uFF09",
1988
- helpSessions: " /sessions \u5217\u51FA\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD\uFF08\u5F53\u524D\u6807\u8BB0\u4E3A \u25B8\uFF09",
1989
- helpForget: " /forget \u4ECE\u78C1\u76D8\u5220\u9664\u5F53\u524D\u4F1A\u8BDD",
1990
- helpNew: " /new \u5168\u65B0\u5F00\u59CB\uFF1A\u4E22\u5F03\u6240\u6709\u4E0A\u4E0B\u6587 + \u6E05\u9664\u6EDA\u52A8\u56DE\u653E",
1991
- helpClear: " /clear \u4EC5\u6E05\u9664\u663E\u793A\u7684\u6EDA\u52A8\u56DE\u653E\uFF08\u4E0A\u4E0B\u6587\u4FDD\u7559 \u2014 \u6A21\u578B\u4ECD\u80FD\u770B\u5230\uFF09",
1992
- helpLoop: " /loop <interval> <prompt> \u6BCF <interval> \u81EA\u52A8\u91CD\u65B0\u63D0\u4EA4 <prompt>\uFF085\u79D2..6\u5C0F\u65F6\uFF09\u3002/loop stop \xB7 \u8F93\u5165\u4EFB\u4F55\u5185\u5BB9\u53D6\u6D88\u3002",
1993
- helpExit: " /exit \u9000\u51FA\uFF08\u522B\u540D\uFF1A/quit, /q\uFF09",
1994
1632
  helpShellTitle: "Shell \u5FEB\u6377\u65B9\u5F0F\uFF1A",
1995
1633
  helpShell: " !<cmd> \u5728\u6C99\u7BB1\u6839\u76EE\u5F55\u8FD0\u884C <cmd>\uFF1B\u8F93\u51FA\u8FDB\u5165\u5BF9\u8BDD",
1996
1634
  helpShellDetail: " \u4EE5\u4FBF\u6A21\u578B\u5728\u4E0B\u4E00\u8F6E\u770B\u5230\u3002\u65E0\u5141\u8BB8\u5217\u8868\u9650\u5236\u3002",
@@ -2017,46 +1655,6 @@ var zhCN = {
2017
1655
  helpSessionsTitle: "\u4F1A\u8BDD\uFF08\u9ED8\u8BA4\u81EA\u52A8\u542F\u7528\uFF0C\u547D\u540D\u4E3A 'default'\uFF09\uFF1A",
2018
1656
  helpSessionCustom: " reasonix chat --session <name> \u4F7F\u7528\u4E0D\u540C\u7684\u547D\u540D\u4F1A\u8BDD",
2019
1657
  helpSessionNone: " reasonix chat --no-session \u7981\u7528\u672C\u6B21\u8FD0\u884C\u7684\u6301\u4E45\u5316",
2020
- helpLimitationTitle: "\u5DF2\u77E5\u9650\u5236\uFF1A",
2021
- helpLimitation1: " \u5728\u4F1A\u8BDD\u4E2D\u9014\u8C03\u6574\u7EC8\u7AEF\u5927\u5C0F\u53EF\u80FD\u4F1A\u5728\u6EDA\u52A8\u56DE\u653E\u4E2D\u5806\u53E0\u5E7D\u7075\u6807\u9898\u5E27",
2022
- helpLimitation2: " \uFF08Ink \u5E93\u7684\u6D3B\u52A8\u533A\u57DF\u6E05\u9664\u672A\u8003\u8651\u65B0\u5BBD\u5EA6\u4E0B\u7684\u884C\u91CD\u6392\uFF09\u3002",
2023
- helpLimitation3: " \u6EDA\u52A8\u5386\u53F2\u4E0D\u53D7\u5F71\u54CD\uFF1B\u8BE5\u7455\u75B5\u7EAF\u5C5E\u89C6\u89C9\u95EE\u9898\uFF0C",
2024
- helpLimitation4: " \u4E0B\u6B21 /clear \u65F6\u6E05\u9664\u3002",
2025
- keysTitle: "\u952E\u76D8\u548C\u63D0\u793A\u5FEB\u6377\u952E\uFF1A",
2026
- keysEnter: " Enter \u63D0\u4EA4\u5F53\u524D\u63D0\u793A",
2027
- keysNewline: " Shift+Enter / Ctrl+J \u63D2\u5165\u6362\u884C\uFF08\u591A\u884C\u63D0\u793A\uFF09",
2028
- keysContinue: " \\<Enter> bash \u98CE\u683C\u7684\u884C\u7EE7\u7EED",
2029
- keysArrow: " \u2190 \u2192 \u2191 \u2193 \u79FB\u52A8\u5149\u6807 / \u5728\u7F13\u51B2\u533A\u8FB9\u754C\u53EC\u56DE\u5386\u53F2",
2030
- keysPage: " PageUp / PageDown \u8DF3\u8F6C\u5230\u6574\u4E2A\u7F13\u51B2\u533A\u7684\u9876\u90E8/\u5E95\u90E8\uFF08\u5927\u6BB5\u7C98\u8D34\u540E\u5F88\u6709\u7528\uFF09",
2031
- keysHomeEnd: " Ctrl+A / Ctrl+E \u8DF3\u8F6C\u5230\u5F53\u524D\u884C\u7684\u5F00\u5934/\u7ED3\u5C3E",
2032
- keysClearLine: " Ctrl+U \u6E05\u9664\u6574\u4E2A\u8F93\u5165\u7F13\u51B2\u533A",
2033
- keysDeleteWord: " Ctrl+W \u5220\u9664\u5149\u6807\u524D\u7684\u5355\u8BCD",
2034
- keysBackspace: " Backspace \u5411\u5DE6\u5220\u9664\uFF1BDelete \u5220\u9664\u5149\u6807\u4E0B\u7684\u5B57\u7B26",
2035
- keysEsc: " Esc \u4E2D\u6B62\u6B63\u5728\u8FDB\u884C\u7684\u8F6E\u6B21",
2036
- keysEditYn: " y / n \u63A5\u53D7/\u62D2\u7EDD\u5F85\u5904\u7406\u7684\u7F16\u8F91\uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09",
2037
- keysEditTab: " Shift+Tab \u5FAA\u73AF\u7F16\u8F91\u95E8\u63A7\uFF1Areview \u2194 AUTO\uFF08\u4EE3\u7801\u6A21\u5F0F\uFF0C\u6301\u4E45\u5316\u5230\u914D\u7F6E\uFF09",
2038
- keysEditUndo: " u \u64A4\u6D88\u6700\u8FD1\u672A\u64A4\u6D88\u7684\u7F16\u8F91\u6279\u5904\u7406\uFF08\u4F1A\u8BDD\u8303\u56F4\uFF0C\u975E\u4EC5\u6A2A\u5E45\uFF09",
2039
- keysPromptTitle: "\u63D0\u793A\u524D\u7F00\uFF1A",
2040
- keysSlash: " /<name> \u659C\u6760\u547D\u4EE4\uFF1BTab/Enter \u4ECE\u5EFA\u8BAE\u5217\u8868\u4E2D\u9009\u62E9",
2041
- keysAtFile: " @<path> \u5C06\u6587\u4EF6\u5185\u8054\u5230 [Referenced files] \u4E0B\uFF08\u4EE3\u7801\u6A21\u5F0F\uFF09\u3002",
2042
- keysAtFilePicker: " \u5C3E\u90E8 `@\u2026` \u6253\u5F00\u6587\u4EF6\u9009\u62E9\u5668\uFF1B\u2191/\u2193 \u5BFC\u822A\uFF0CTab/Enter \u9009\u62E9\u3002",
2043
- keysAtUrl: " @https://... \u83B7\u53D6 URL\uFF0C\u5265\u79BB HTML\uFF0C\u5185\u8054\u5230 [Referenced URLs] \u4E0B\u3002",
2044
- keysAtUrlCache: " \u6BCF\u4F1A\u8BDD\u7F13\u5B58 \u2014 \u76F8\u540C URL \u83B7\u53D6\u4E24\u6B21\u53EA\u53D6\u4E00\u6B21\u3002",
2045
- keysBang: " !<cmd> \u5728\u6C99\u7BB1\u6839\u76EE\u5F55\u8FD0\u884C <cmd>\uFF1B\u8F93\u51FA\u8FDB\u5165\u4E0A\u4E0B\u6587",
2046
- keysBangDetail: " \u4EE5\u4FBF\u6A21\u578B\u5728\u4E0B\u4E00\u8F6E\u770B\u5230\u3002\u65E0\u5141\u8BB8\u5217\u8868\u9650\u5236\u3002",
2047
- keysHash: " #<note> \u5C06 <note> \u8FFD\u52A0\u5230 <project>/REASONIX.md\uFF08\u53EF\u63D0\u4EA4\uFF0C\u56E2\u961F\u5171\u4EAB\uFF09\u3002",
2048
- keysHashGlobal: " #g <note> \u5C06 <note> \u8FFD\u52A0\u5230 ~/.reasonix/REASONIX.md\uFF08\u5168\u5C40\uFF0C\u4E0D\u63D0\u4EA4\uFF09\u3002",
2049
- keysHashBoth: " \u4E24\u8005\u90FD\u56FA\u5B9A\u5230\u6BCF\u4E2A\u672A\u6765\u4F1A\u8BDD\u7684\u4E0D\u53EF\u53D8\u524D\u7F00\u4E2D\u3002",
2050
- keysHashEscape: " \u4F7F\u7528 `\\#literal` \u5982\u679C\u60A8\u786E\u5B9E\u60F3\u53D1\u9001 `#` \u6807\u9898\u7ED9\u6A21\u578B\u3002",
2051
- keysPickersTitle: "\u9009\u62E9\u5668\uFF08\u659C\u6760 + @\u63D0\u53CA\uFF09\uFF1A",
2052
- keysPickerNav: " \u2191 / \u2193 \u5BFC\u822A\u5EFA\u8BAE\u5217\u8868",
2053
- keysPickerTab: " Tab \u63D2\u5165\u9AD8\u4EAE\u9879\u76EE\u4F46\u4E0D\u63D0\u4EA4",
2054
- keysPickerEnter: " Enter \u63D2\u5165\u5E76\uFF08\u659C\u6760\uFF09\u8FD0\u884C\uFF0C\uFF08@\uFF09\u7EE7\u7EED\u7F16\u8F91",
2055
- keysMcpTitle: "MCP \u63A2\u7D22\uFF1A",
2056
- keysMcpServers: " /mcp \u670D\u52A1\u5668 + \u5DE5\u5177/\u8D44\u6E90/\u63D0\u793A\u8BA1\u6570",
2057
- keysMcpResource: " /resource [uri] \u6D4F\u89C8\u5E76\u8BFB\u53D6 MCP \u670D\u52A1\u5668\u66B4\u9732\u7684\u8D44\u6E90",
2058
- keysMcpPrompt: " /prompt [name] \u6D4F\u89C8\u5E76\u83B7\u53D6 MCP \u670D\u52A1\u5668\u66B4\u9732\u7684\u63D0\u793A",
2059
- keysUseful: "\u5E38\u7528\u659C\u6760\u547D\u4EE4\uFF1A/help \xB7 /context \xB7 /stats \xB7 /compact \xB7 /new \xB7 /exit",
2060
1658
  retryNone: "\u6CA1\u6709\u53EF\u91CD\u8BD5\u7684\u5185\u5BB9 \u2014 \u6B64\u4F1A\u8BDD\u65E5\u5FD7\u4E2D\u6CA1\u6709\u5148\u524D\u7684\u7528\u6237\u6D88\u606F\u3002",
2061
1659
  retryInfo: '\u25B8 \u91CD\u8BD5\u4E2D\uFF1A"{preview}"',
2062
1660
  loopTuiOnly: "/loop \u4EC5\u5728\u4EA4\u4E92\u5F0F TUI \u4E2D\u53EF\u7528\uFF08\u4E0D\u5728 run/replay \u4E2D\uFF09\u3002",
@@ -2106,9 +1704,6 @@ var zhCN = {
2106
1704
  planCodeOnly: "/plan \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528 \u2014 \u804A\u5929\u6A21\u5F0F\u4E0D\u9650\u5236\u5DE5\u5177\u5199\u5165\u3002",
2107
1705
  planOn: "\u25B8 \u8BA1\u5212\u6A21\u5F0F\u5F00\u542F \u2014 \u5199\u5165\u5DE5\u5177\u88AB\u9650\u5236\uFF1B\u6A21\u578B\u5FC5\u987B\u5148\u8C03\u7528 `submit_plan` \u624D\u80FD\u6267\u884C\u4EFB\u4F55\u64CD\u4F5C\u3002\uFF08\u6A21\u578B\u4E5F\u53EF\u4EE5\u5728\u8BA1\u5212\u6A21\u5F0F\u5173\u95ED\u65F6\u81EA\u4E3B\u8C03\u7528 submit_plan \u5904\u7406\u5927\u578B\u4EFB\u52A1 \u2014 \u6B64\u5F00\u5173\u662F\u66F4\u5F3A\u7684\u663E\u5F0F\u7EA6\u675F\u3002\uFF09\u8F93\u5165 /plan off \u9000\u51FA\u3002",
2108
1706
  planOff: "\u25B8 \u8BA1\u5212\u6A21\u5F0F\u5173\u95ED \u2014 \u5199\u5165\u5DE5\u5177\u518D\u6B21\u53EF\u7528\u3002\u6A21\u578B\u4ECD\u53EF\u4E3A\u5927\u578B\u4EFB\u52A1\u81EA\u4E3B\u63D0\u51FA\u8BA1\u5212\u3002",
2109
- applyPlanCodeOnly: "/apply-plan \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528\u3002",
2110
- applyPlanInfo: "\u25B8 \u8BA1\u5212\u5DF2\u6279\u51C6 \u2014 \u6B63\u5728\u6267\u884C",
2111
- applyPlanResubmit: "\u4E0A\u65B9\u7684\u8BA1\u5212\u5DF2\u88AB\u6279\u51C6\u3002\u7ACB\u5373\u6267\u884C\u3002\u60A8\u5DF2\u9000\u51FA\u8BA1\u5212\u6A21\u5F0F \u2014 \u6839\u636E\u9700\u8981\u4F7F\u7528 edit_file / write_file / run_command\u3002\u9664\u975E\u53D1\u73B0\u5177\u4F53\u539F\u56E0\uFF0C\u5426\u5219\u8BF7\u9075\u5FAA\u8BA1\u5212\uFF1B\u5982\u679C\u786E\u5B9E\u9700\u8981\u504F\u79BB\uFF0C\u8BF7\u544A\u77E5\u5E76\u7B49\u5F85\u56DE\u590D\u540E\u518D\u8FDB\u884C\u3002",
2112
1707
  modeCodeOnly: "/mode \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528\u3002",
2113
1708
  modeUsage: "\u7528\u6CD5\uFF1A/mode <review|auto|yolo> \uFF08Shift+Tab \u4E5F\u53EF\u5FAA\u73AF\uFF09",
2114
1709
  modeYolo: "\u25B8 \u7F16\u8F91\u6A21\u5F0F\uFF1AYOLO \u2014 \u7F16\u8F91\u548C Shell \u547D\u4EE4\u81EA\u52A8\u8FD0\u884C\uFF0C\u65E0\u63D0\u793A\u3002/undo \u4ECD\u53EF\u56DE\u6EDA\u7F16\u8F91\u3002\u8BF7\u8C28\u614E\u4F7F\u7528\u3002",
@@ -2134,31 +1729,20 @@ var zhCN = {
2134
1729
  restoreInfo: '\u25B8 \u5DF2\u6062\u590D "{name}"\uFF08{id}\uFF09\uFF0C\u6765\u81EA {when}',
2135
1730
  restoreWrote: " \xB7 \u5199\u56DE\u4E86 {count} \u4E2A\u6587\u4EF6",
2136
1731
  restoreRemoved: " \xB7 \u79FB\u9664\u4E86 {count} \u4E2A\u6587\u4EF6\uFF08\u68C0\u67E5\u70B9\u65F6\u4E0D\u5B58\u5728\uFF09",
2137
- restoreSkipped: " \u2717 \u8DF3\u8FC7\u4E86 {count} \u4E2A\u6587\u4EF6\uFF1A"
1732
+ restoreSkipped: " \u2717 \u8DF3\u8FC7\u4E86 {count} \u4E2A\u6587\u4EF6\uFF1A",
1733
+ cwdCodeOnly: "/cwd \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528\u3002",
1734
+ cwdUsage: "\u7528\u6CD5\uFF1A/cwd <path> \uFF08\u5F53\u524D\u6839\u76EE\u5F55\uFF1A{current}\uFF09\u3002\u91CD\u65B0\u6307\u5411 filesystem / shell / memory \u5DE5\u5177\u5230 <path>\u3002",
1735
+ cwdUsageNoCurrent: "\u7528\u6CD5\uFF1A/cwd <path> \u5C06\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\u5207\u6362\u5230 <path>\u3002"
2138
1736
  },
2139
1737
  model: {
2140
1738
  modelHint: "\u5C1D\u8BD5 deepseek-v4-flash \u6216 deepseek-v4-pro \u2014 \u8FD0\u884C /models \u83B7\u53D6\u5B9E\u65F6\u5217\u8868",
2141
1739
  modelUsage: "\u7528\u6CD5\uFF1A/model <id> \uFF08{hint}\uFF09",
2142
1740
  modelNotInCatalog: "model \u2192 {id} \uFF08\u26A0 \u4E0D\u5728\u83B7\u53D6\u7684\u76EE\u5F55\u4E2D\uFF1A{list}\u3002\u5982\u679C\u8FD9\u662F\u9519\u8BEF\u7684\uFF0C\u4E0B\u6B21\u8C03\u7528\u5C06\u8FD4\u56DE 400 \u2014 \u8FD0\u884C /models \u5237\u65B0\u3002\uFF09",
2143
1741
  modelSet: "model \u2192 {id}",
2144
- modelsFetching: "\u6B63\u5728\u4ECE DeepSeek \u83B7\u53D6 /models\u2026 \u7A0D\u540E\u518D\u8FD0\u884C /models\u3002\u5982\u679C\u6301\u7EED\u4E3A\u7A7A\uFF0C\u60A8\u7684 API \u5BC6\u94A5\u53EF\u80FD\u7F3A\u5C11\u6743\u9650\u6216\u7F51\u7EDC\u88AB\u963B\u6B62\u3002",
2145
- modelsEmpty: "DeepSeek /models \u8FD4\u56DE\u4E86\u7A7A\u5217\u8868\u3002\u518D\u8BD5 /models\uFF0C\u6216\u5728 api-docs.deepseek.com \u68C0\u67E5\u60A8\u7684\u8D26\u6237\u72B6\u6001\u3002",
2146
- modelsHeader: "\u53EF\u7528\u6A21\u578B\uFF08DeepSeek /models \xB7 \u5171 {count} \u4E2A\uFF09\uFF1A",
2147
- modelsCurrent: "\u25B8 {id} \uFF08\u5F53\u524D\uFF09",
2148
- modelsSwitch: "\u5207\u6362\u65B9\u5F0F\uFF1A/model <id>",
2149
- harvestOn: "harvest \u2192 \u5F00\u542F \uFF08Pillar-2 \u8BA1\u5212\u72B6\u6001\u63D0\u53D6 \xB7 \u6BCF\u8F6E\u989D\u5916 1 \u6B21\u5EC9\u4EF7 flash \u8C03\u7528 \xB7 \u4EC5\u624B\u52A8\u9009\u62E9\uFF1B\u65E0\u9884\u8BBE\u4F1A\u5F00\u542F\u5B83\uFF09",
2150
- harvestOff: "harvest \u2192 \u5173\u95ED",
2151
1742
  presetAuto: "preset \u2192 auto \uFF08v4-flash \u2192 v4-pro \u5728\u56F0\u96BE\u8F6E\u6B21\u5207\u6362 \xB7 \u9ED8\u8BA4\uFF09",
2152
1743
  presetFlash: "preset \u2192 flash \uFF08\u59CB\u7EC8\u4F7F\u7528 v4-flash \xB7 \u6700\u4FBF\u5B9C \xB7 /pro \u4ECD\u53EF\u4E34\u65F6\u63D0\u5347\u4E00\u8F6E\uFF09",
2153
1744
  presetPro: "preset \u2192 pro \uFF08\u59CB\u7EC8\u4F7F\u7528 v4-pro \xB7 \u7EA6 3 \u500D flash \xB7 \u7528\u4E8E\u56F0\u96BE\u7684\u591A\u8F6E\u5DE5\u4F5C\uFF09",
2154
1745
  presetUsage: "\u7528\u6CD5\uFF1A/preset <auto|flash|pro>",
2155
- branchOff: "branch \u2192 \u5173\u95ED",
2156
- branchUsage: "\u7528\u6CD5\uFF1A/branch <N> \uFF08N>=2\uFF0C\u6216 'off'\uFF09",
2157
- branchCapped: "branch \u9884\u7B97\u4E0A\u9650\u4E3A 8\uFF0C\u9632\u6B62\u6210\u672C\u5931\u63A7",
2158
- branchSet: "branch \u2192 {n} \uFF08\u6BCF\u8F6E\u8FD0\u884C {n} \u4E2A\u5E76\u884C\u91C7\u6837 \xB7 {n} \u500D\u6BCF\u8F6E\u6210\u672C \xB7 \u7981\u7528\u6D41\u5F0F \xB7 \u4EC5\u624B\u52A8\uFF0C\u65E0\u9884\u8BBE\u542F\u7528\u5206\u652F\uFF09",
2159
- effortStatus: "reasoning_effort \u2192 {effort} \uFF08\u4F7F\u7528 /effort high \u66F4\u4FBF\u5B9C/\u66F4\u5FEB\uFF0C/effort max \u4E3A\u667A\u80FD\u4F53\u7EA7\u9ED8\u8BA4 \xB7 \u8DE8\u91CD\u542F\u6301\u4E45\u5316\uFF09",
2160
- effortUsage: "\u7528\u6CD5\uFF1A/effort <high|max>",
2161
- effortSet: "reasoning_effort \u2192 {effort}\uFF08\u5DF2\u6301\u4E45\u5316\uFF09",
2162
1746
  proNothingArmed: "\u672A\u542F\u7528 \u2014 /pro \u4E0D\u5E26\u53C2\u6570\u5C06\u4E3A\u4E0B\u4E00\u8F6E\u542F\u7528 pro",
2163
1747
  proDisarmed: "\u25B8 /pro \u5DF2\u89E3\u9664 \u2014 \u4E0B\u4E00\u8F6E\u56DE\u9000\u5230\u5F53\u524D\u9884\u8BBE",
2164
1748
  proUsage: "\u7528\u6CD5\uFF1A/pro \u4E3A\u4E0B\u4E00\u8F6E\u542F\u7528 pro\uFF08\u4E00\u6B21\u6027\uFF0C\u81EA\u52A8\u89E3\u9664\uFF09\n /pro off \u5728\u4E0B\u4E00\u8F6E\u524D\u53D6\u6D88\u542F\u7528\u72B6\u6001",
@@ -2170,18 +1754,6 @@ var zhCN = {
2170
1754
  budgetExhausted: "\u25B2 budget \u2192 ${cap} \u4F46\u5DF2\u82B1\u8D39 ${spent}\u3002\u4E0B\u4E00\u8F6E\u5C06\u88AB\u62D2\u7EDD \u2014 \u63D0\u9AD8\u4E0A\u9650\u4EE5\u7EE7\u7EED\uFF0C\u6216\u7ED3\u675F\u4F1A\u8BDD\u3002",
2171
1755
  budgetSet: "budget \u2192 ${cap} \uFF08\u8FC4\u4ECA\uFF1A${spent} \xB7 80% \u65F6\u8B66\u544A\uFF0C100% \u65F6\u62D2\u7EDD\u4E0B\u4E00\u8F6E \xB7 /budget off \u6E05\u9664\uFF09"
2172
1756
  },
2173
- sessions: {
2174
- forgetNoSession: "\u4E0D\u5728\u4F1A\u8BDD\u4E2D \u2014 \u65E0\u5185\u5BB9\u53EF\u9057\u5FD8",
2175
- forgetInfo: '\u25B8 \u5DF2\u5220\u9664\u4F1A\u8BDD "{name}" \u2014 \u5F53\u524D\u5C4F\u5E55\u4ECD\u663E\u793A\u5BF9\u8BDD\uFF0C\u4F46\u4E0B\u6B21\u542F\u52A8\u5C06\u5168\u65B0\u5F00\u59CB',
2176
- forgetFailed: '\u65E0\u6CD5\u5220\u9664\u4F1A\u8BDD "{name}"\uFF08\u5DF2\u6D88\u5931\uFF1F\uFF09',
2177
- renameUsage: "\u7528\u6CD5\uFF1A/rename <new-name>",
2178
- renameNoSession: "\u4E0D\u5728\u4F1A\u8BDD\u4E2D \u2014 \u65E0\u5185\u5BB9\u53EF\u91CD\u547D\u540D",
2179
- renameFailed: '\u65E0\u6CD5\u91CD\u547D\u540D \u2014 "{name}" \u5DF2\u5B58\u5728\u6216\u6E05\u7406\u540E\u4E0E\u5F53\u524D\u4F1A\u8BDD ID \u76F8\u540C',
2180
- renameInfo: '\u25B8 \u4F1A\u8BDD\u5DF2\u91CD\u547D\u540D\u4E3A \u2192 "{name}"\u3002\u91CD\u542F TUI \u4EE5\u4F7F\u7528\u65B0\u540D\u79F0\u3002',
2181
- resumeUsage: "\u7528\u6CD5\uFF1A/resume <session-name> \u2014 \u4F7F\u7528 /sessions \u5217\u51FA",
2182
- resumeNotFound: '\u6CA1\u6709\u540D\u4E3A "{name}" \u7684\u4F1A\u8BDD \u2014 \u4F7F\u7528 /sessions \u5217\u51FA',
2183
- resumeInfo: '\u25B8 \u8981\u6062\u590D "{name}"\uFF0C\u8BF7\u9000\u51FA\u5E76\u8FD0\u884C\uFF1Areasonix chat --session {name}\n \uFF08\u4F1A\u8BDD\u4E2D\u5207\u6362\u9700\u8981\u91CD\u542F\uFF0C\u4EE5\u4FBF\u6D88\u606F\u65E5\u5FD7\u53EF\u4EE5\u5E72\u51C0\u5730\u56DE\u9000\uFF09'
2184
- },
2185
1757
  permissions: {
2186
1758
  mutateCodeOnly: "/permissions add / remove / clear \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528 \u2014 \u5B83\u4EEC\u7F16\u8F91\u9879\u76EE\u8303\u56F4\u7684\u5141\u8BB8\u5217\u8868\uFF08`~/.reasonix/config.json` projects[<root>].shellAllowed\uFF09\u3002",
2187
1759
  addUsage: '\u7528\u6CD5\uFF1A/permissions add <prefix> \uFF08\u591A token \u53EF\u7528\uFF1A/permissions add "git push origin"\uFF09',
@@ -2223,13 +1795,6 @@ var zhCN = {
2223
1795
  starting: "\u25B8 \u6B63\u5728\u542F\u52A8\u4EEA\u8868\u677F\u670D\u52A1\u5668\u2026"
2224
1796
  },
2225
1797
  observability: {
2226
- thinkEmpty: "\u672A\u7F13\u5B58\u63A8\u7406\u5185\u5BB9\u3002`/think` \u663E\u793A\u6700\u8FD1\u4E00\u8F6E\u7684\u5B8C\u6574\u601D\u8003\u6A21\u5F0F\u601D\u7EF4 \u2014 \u4EC5\u601D\u8003\u6A21\u5F0F\u6A21\u578B\uFF08deepseek-v4-flash / -v4-pro / -reasoner\uFF09\u4EA7\u751F\u5B83\uFF0C\u4E14\u4EC5\u5728\u8F6E\u6B21\u5B8C\u6210\u540E\u3002",
2227
- thinkInfo: "\u21B3 \u5B8C\u6574\u601D\u8003\uFF08{count} \u5B57\u7B26\uFF09\uFF1A",
2228
- toolEmpty: "\u6B64\u4F1A\u8BDD\u4E2D\u5C1A\u65E0\u5DE5\u5177\u8C03\u7528\u3002`/tool` \u5728\u6A21\u578B\u5B9E\u9645\u4F7F\u7528\u5DE5\u5177\u540E\u5217\u51FA\u5B83\u4EEC\uFF1B`/tool N` \u8F6C\u50A8\u7B2C N \u4E2A\u6700\u8FD1\u7684\u5B8C\u6574\uFF08\u672A\u622A\u65AD\uFF09\u8F93\u51FA\u3002",
2229
- toolUsage: "\u7528\u6CD5\uFF1A/tool [N] \uFF08\u65E0\u53C2\u6570 \u2192 \u5217\u8868\uFF1BN=1 \u2192 \u6700\u8FD1\u7684\u5B8C\u6574\u7ED3\u679C\uFF0CN=2 \u2192 \u4E0A\u4E00\u4E2A\uFF0C\u2026\uFF09",
2230
- toolOob: "\u5386\u53F2\u4E2D\u4EC5\u6709 {count} \u6B21\u5DE5\u5177\u8C03\u7528 \u2014 \u8BF7\u6C42\u4E86 #{n}\u3002\u5C1D\u8BD5\u4E0D\u5E26\u53C2\u6570\u7684 /tool \u67E5\u770B\u5217\u8868\u3002",
2231
- toolNotFound: "\u65E0\u6CD5\u8BFB\u53D6\u5DE5\u5177\u8C03\u7528 #{n}",
2232
- toolInfo: "\u21B3 tool<{name}> #{n}\uFF08{chars} \u5B57\u7B26\uFF09\uFF1A",
2233
1798
  contextInfo: "\u4E0A\u4E0B\u6587\uFF1A~{total} / {max}\uFF08{pct}%\uFF09\xB7 \u7CFB\u7EDF {sys} \xB7 \u5DE5\u5177 {tools} \xB7 \u65E5\u5FD7 {log}",
2234
1799
  compactStarting: "\u25B8 \u6B63\u5728\u6298\u53E0\u65E7\u8F6E\u6B21\u4E3A\u6458\u8981\u2026",
2235
1800
  compactNoop: "\u25B8 \u65E0\u9700\u6298\u53E0 \u2014 \u65E5\u5FD7\u5DF2\u8DB3\u591F\u5C0F\uFF0C\u6216\u6700\u8FD1\u8F6E\u6B21\u672C\u8EAB\u5DF2\u8D85\u8FC7\u9884\u7B97\u3002",
@@ -2243,7 +1808,7 @@ var zhCN = {
2243
1808
  costLikely: " \u53EF\u80FD\uFF08{pct}% \u4F1A\u8BDD\u7F13\u5B58\u547D\u4E2D\uFF09\uFF1A{input} \u8F93\u5165 + ~{output} \u8F93\u51FA \u2248 {total}",
2244
1809
  costLikelyCold: " \u53EF\u80FD\uFF1A\u5728\u7F13\u5B58\u586B\u5145\u524D\u4E0E\u6700\u574F\u60C5\u51B5\u76F8\u540C\uFF08\u65E0\u5DF2\u5B8C\u6210\u7684\u8F6E\u6B21\uFF09",
2245
1810
  statusModel: " \u6A21\u578B {model}",
2246
- statusFlags: " \u6807\u5FD7 harvest={harvest} \xB7 branch={branch} \xB7 stream={stream} \xB7 effort={effort}",
1811
+ statusFlags: " \u6807\u5FD7 stream={stream} \xB7 effort={effort}",
2247
1812
  statusCtx: " \u4E0A\u4E0B\u6587 {bar} {used}/{max}\uFF08{pct}%\uFF09",
2248
1813
  statusCtxNone: " \u4E0A\u4E0B\u6587 \u5C1A\u65E0\u8F6E\u6B21",
2249
1814
  statusCost: " \u6210\u672C ${cost} \xB7 \u7F13\u5B58 {bar} {pct}% \xB7 \u8F6E\u6B21 {turns}",
@@ -2345,10 +1910,6 @@ var zhCN = {
2345
1910
  existsPinned: " \u56FA\u5B9A\u5230\u6BCF\u6B21\u542F\u52A8\u7684\u7CFB\u7EDF\u63D0\u793A\u8BCD\u4E2D\u3002",
2346
1911
  info: "\u25B8 /init \u2014 \u6A21\u578B\u5C06\u626B\u63CF\u9879\u76EE\u5E76\u5408\u6210 REASONIX.md\u3002\n \u7ED3\u679C\u5C06\u4F5C\u4E3A\u5F85\u5904\u7406\u7684\u7F16\u8F91\uFF1B\u4F7F\u7528 /apply \u6216 /walk \u5BA1\u67E5\u3002"
2347
1912
  },
2348
- semantic: {
2349
- codeOnly: "/semantic \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528\uFF08\u9700\u8981\u9879\u76EE\u6839\u76EE\u5F55\uFF09\u3002",
2350
- checking: "\u25B8 \u6B63\u5728\u68C0\u67E5 semantic_search \u72B6\u6001\u2026"
2351
- },
2352
1913
  webSearchEngine: {
2353
1914
  currentEngine: "\u5F53\u524D\u7F51\u9875\u641C\u7D22\u5F15\u64CE\uFF1A{engine}",
2354
1915
  endpoint: "SearXNG \u7AEF\u70B9\uFF1A{url}",
@@ -3579,16 +3140,6 @@ var ContextManager = class {
3579
3140
  }
3580
3141
  };
3581
3142
 
3582
- // src/loop/branch.ts
3583
- function summarizeBranch(chosen, samples) {
3584
- return {
3585
- budget: samples.length,
3586
- chosenIndex: chosen.index,
3587
- uncertainties: samples.map((s) => s.planState.uncertainties.length),
3588
- temperatures: samples.map((s) => s.temperature)
3589
- };
3590
- }
3591
-
3592
3143
  // src/loop/errors.ts
3593
3144
  function formatLoopError(err, probe) {
3594
3145
  const msg = err.message ?? "";
@@ -4351,13 +3902,9 @@ var CacheFirstLoop = class {
4351
3902
  stats = new SessionStats();
4352
3903
  repair;
4353
3904
  // Mutable via configure() — slash commands in the TUI / library callers tweak
4354
- // these mid-session so users don't have to restart to try harvest or branch.
3905
+ // these mid-session so users don't have to restart.
4355
3906
  model;
4356
3907
  stream;
4357
- harvestEnabled;
4358
- harvestOptions;
4359
- branchEnabled;
4360
- branchOptions;
4361
3908
  reasoningEffort;
4362
3909
  autoEscalate = true;
4363
3910
  budgetUsd;
@@ -4395,19 +3942,8 @@ var CacheFirstLoop = class {
4395
3942
  this.hooks = opts.hooks ?? [];
4396
3943
  this.hookCwd = opts.hookCwd ?? process.cwd();
4397
3944
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
4398
- if (typeof opts.branch === "number") {
4399
- this.branchOptions = { budget: opts.branch };
4400
- } else if (opts.branch && typeof opts.branch === "object") {
4401
- this.branchOptions = opts.branch;
4402
- } else {
4403
- this.branchOptions = {};
4404
- }
4405
- this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
4406
- const harvestForced = this.branchEnabled;
4407
- this.harvestEnabled = harvestForced || opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
4408
- this.harvestOptions = typeof opts.harvest === "object" && opts.harvest !== null ? opts.harvest : this.branchOptions.harvestOptions ?? {};
4409
3945
  this._streamPreference = opts.stream ?? true;
4410
- this.stream = this.branchEnabled ? false : this._streamPreference;
3946
+ this.stream = this._streamPreference;
4411
3947
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
4412
3948
  const registry = this.tools;
4413
3949
  const isMutating = (call) => {
@@ -4525,29 +4061,12 @@ var CacheFirstLoop = class {
4525
4061
  }
4526
4062
  configure(opts) {
4527
4063
  if (opts.model !== void 0) this.model = opts.model;
4528
- if (opts.stream !== void 0) this._streamPreference = opts.stream;
4064
+ if (opts.stream !== void 0) {
4065
+ this._streamPreference = opts.stream;
4066
+ this.stream = opts.stream;
4067
+ }
4529
4068
  if (opts.reasoningEffort !== void 0) this.reasoningEffort = opts.reasoningEffort;
4530
4069
  if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
4531
- if (opts.branch !== void 0) {
4532
- if (typeof opts.branch === "number") {
4533
- this.branchOptions = { budget: opts.branch };
4534
- } else if (opts.branch && typeof opts.branch === "object") {
4535
- this.branchOptions = opts.branch;
4536
- } else {
4537
- this.branchOptions = {};
4538
- }
4539
- this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
4540
- }
4541
- if (opts.harvest !== void 0) {
4542
- const want = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
4543
- this.harvestEnabled = want || this.branchEnabled;
4544
- if (typeof opts.harvest === "object" && opts.harvest !== null) {
4545
- this.harvestOptions = opts.harvest;
4546
- }
4547
- } else if (this.branchEnabled) {
4548
- this.harvestEnabled = true;
4549
- }
4550
- this.stream = this.branchEnabled ? false : this._streamPreference;
4551
4070
  }
4552
4071
  /** `null` disables the cap; any change re-arms the 80% warning. */
4553
4072
  setBudget(usd) {
@@ -4789,89 +4308,8 @@ ${reason}`
4789
4308
  let reasoningContent = "";
4790
4309
  let toolCalls = [];
4791
4310
  let usage = null;
4792
- let branchSummary;
4793
- let preHarvestedPlanState;
4794
4311
  try {
4795
- if (this.branchEnabled) {
4796
- const budget = this.branchOptions.budget ?? 1;
4797
- yield {
4798
- turn: this._turn,
4799
- role: "branch_start",
4800
- content: "",
4801
- branchProgress: {
4802
- completed: 0,
4803
- total: budget,
4804
- latestIndex: -1,
4805
- latestTemperature: -1,
4806
- latestUncertainties: -1
4807
- }
4808
- };
4809
- const queue = [];
4810
- let waiter = null;
4811
- const onSampleDone = (sample) => {
4812
- if (waiter) {
4813
- const w = waiter;
4814
- waiter = null;
4815
- w(sample);
4816
- } else {
4817
- queue.push(sample);
4818
- }
4819
- };
4820
- const callModel = this.modelForCurrentCall();
4821
- const branchPromise = runBranches(
4822
- this.client,
4823
- {
4824
- model: callModel,
4825
- messages,
4826
- tools: toolSpecs.length ? toolSpecs : void 0,
4827
- signal,
4828
- thinking: thinkingModeForModel(callModel),
4829
- reasoningEffort: this.reasoningEffort
4830
- },
4831
- {
4832
- ...this.branchOptions,
4833
- harvestOptions: this.harvestOptions,
4834
- onSampleDone
4835
- }
4836
- );
4837
- for (let k = 0; k < budget; k++) {
4838
- const sample = queue.shift() ?? await new Promise((resolve10) => {
4839
- waiter = resolve10;
4840
- });
4841
- yield {
4842
- turn: this._turn,
4843
- role: "branch_progress",
4844
- content: "",
4845
- branchProgress: {
4846
- completed: k + 1,
4847
- total: budget,
4848
- latestIndex: sample.index,
4849
- latestTemperature: sample.temperature,
4850
- latestUncertainties: sample.planState.uncertainties.length
4851
- }
4852
- };
4853
- }
4854
- const result = await branchPromise;
4855
- assistantContent = result.chosen.response.content;
4856
- reasoningContent = result.chosen.response.reasoningContent ?? "";
4857
- toolCalls = result.chosen.response.toolCalls;
4858
- const agg = aggregateBranchUsage(result.samples);
4859
- usage = new Usage(
4860
- agg.promptTokens,
4861
- agg.completionTokens,
4862
- agg.totalTokens,
4863
- agg.promptCacheHitTokens,
4864
- agg.promptCacheMissTokens
4865
- );
4866
- preHarvestedPlanState = result.chosen.planState;
4867
- branchSummary = summarizeBranch(result.chosen, result.samples);
4868
- yield {
4869
- turn: this._turn,
4870
- role: "branch_done",
4871
- content: "",
4872
- branch: branchSummary
4873
- };
4874
- } else if (this.stream) {
4312
+ if (this.stream) {
4875
4313
  const callBuf = /* @__PURE__ */ new Map();
4876
4314
  const readyIndices = /* @__PURE__ */ new Set();
4877
4315
  const callModel = this.modelForCurrentCall();
@@ -5001,8 +4439,6 @@ ${reason}`
5001
4439
  reasoningContent = "";
5002
4440
  toolCalls = [];
5003
4441
  usage = null;
5004
- branchSummary = void 0;
5005
- preHarvestedPlanState = void 0;
5006
4442
  iter--;
5007
4443
  continue;
5008
4444
  }
@@ -5016,14 +4452,6 @@ ${reason}`
5016
4452
  pendingUser = null;
5017
4453
  }
5018
4454
  this.scratch.reasoning = reasoningContent || null;
5019
- if (!preHarvestedPlanState && this.harvestEnabled && (reasoningContent?.trim().length ?? 0) >= 40) {
5020
- yield {
5021
- turn: this._turn,
5022
- role: "status",
5023
- content: t("loop.harvestStatus")
5024
- };
5025
- }
5026
- const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions, signal) : emptyPlanState();
5027
4455
  const { calls: repairedCalls, report } = this.repair.process(
5028
4456
  toolCalls,
5029
4457
  reasoningContent || null,
@@ -5042,9 +4470,7 @@ ${reason}`
5042
4470
  role: "assistant_final",
5043
4471
  content: assistantContent,
5044
4472
  stats: turnStats,
5045
- planState,
5046
- repair: report,
5047
- branch: branchSummary
4473
+ repair: report
5048
4474
  };
5049
4475
  if (this.noteToolFailureSignal("", report)) {
5050
4476
  yield {
@@ -5278,6 +4704,7 @@ function ignoredByLayers(layers, abs, isDir) {
5278
4704
 
5279
4705
  // src/at-mentions.ts
5280
4706
  var DEFAULT_AT_MENTION_MAX_BYTES = 64 * 1024;
4707
+ var DEFAULT_AT_DIR_MAX_ENTRIES = 200;
5281
4708
  var DEFAULT_PICKER_IGNORE_DIRS = [
5282
4709
  "node_modules",
5283
4710
  ".git",
@@ -5333,6 +4760,16 @@ function listFilesWithStatsSync(root, opts = {}) {
5333
4760
  } catch {
5334
4761
  }
5335
4762
  out.push({ path: relPath, mtimeMs });
4763
+ } else if (ent.isSymbolicLink()) {
4764
+ let target = null;
4765
+ try {
4766
+ target = statSync2(absPath);
4767
+ } catch {
4768
+ continue;
4769
+ }
4770
+ if (!target.isFile()) continue;
4771
+ if (ignoredByLayers(effectiveLayers, absPath, false)) continue;
4772
+ out.push({ path: relPath, mtimeMs: target.mtimeMs });
5336
4773
  }
5337
4774
  }
5338
4775
  };
@@ -5373,7 +4810,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
5373
4810
  if (out.length >= maxResults) return;
5374
4811
  }
5375
4812
  await walk2(absPath, relPath, effectiveLayers);
5376
- } else if (ent.isFile()) {
4813
+ } else if (ent.isFile() || ent.isSymbolicLink()) {
5377
4814
  fileEnts.push(ent);
5378
4815
  }
5379
4816
  }
@@ -5393,14 +4830,18 @@ async function statBatch(ents, dirAbs, dirRel, out, maxResults, layers) {
5393
4830
  }
5394
4831
  const stats = await Promise.all(
5395
4832
  accepted.map(
5396
- (e) => stat(join5(dirAbs, e.name)).then((s) => s.mtimeMs).catch(() => 0)
4833
+ (e) => stat(join5(dirAbs, e.name)).then((s) => ({ mtimeMs: s.mtimeMs, isFile: s.isFile() })).catch(() => null)
5397
4834
  )
5398
4835
  );
5399
4836
  for (let i = 0; i < accepted.length; i++) {
5400
4837
  const ent = accepted[i];
4838
+ const s = stats[i];
4839
+ if (ent.isSymbolicLink()) {
4840
+ if (!s || !s.isFile) continue;
4841
+ }
5401
4842
  out.push({
5402
4843
  path: dirRel ? `${dirRel}/${ent.name}` : ent.name,
5403
- mtimeMs: stats[i] ?? 0
4844
+ mtimeMs: s?.mtimeMs ?? 0
5404
4845
  });
5405
4846
  }
5406
4847
  }
@@ -5438,15 +4879,25 @@ function rankPickerCandidates(files, query, limitOrOpts) {
5438
4879
  for (const e of entries) {
5439
4880
  const lower = e.path.toLowerCase();
5440
4881
  const hit = lower.indexOf(needle);
5441
- if (hit < 0) continue;
5442
- const slash = lower.lastIndexOf("/");
5443
- const base = slash >= 0 ? lower.slice(slash + 1) : lower;
5444
- let score = 2;
5445
- if (base.startsWith(needle)) score = 0;
5446
- else if (lower.startsWith(needle)) score = 1;
4882
+ if (hit >= 0) {
4883
+ const slash = lower.lastIndexOf("/");
4884
+ const base = slash >= 0 ? lower.slice(slash + 1) : lower;
4885
+ let cls = 2;
4886
+ if (base.startsWith(needle)) cls = 0;
4887
+ else if (lower.startsWith(needle)) cls = 1;
4888
+ scored.push({
4889
+ path: e.path,
4890
+ score: cls * 1e4 + Math.min(hit, 9999),
4891
+ mtimeMs: e.mtimeMs,
4892
+ recent: recent.has(e.path)
4893
+ });
4894
+ continue;
4895
+ }
4896
+ const fuzzy = fuzzySubseqScore(needle, lower);
4897
+ if (fuzzy === null) continue;
5447
4898
  scored.push({
5448
4899
  path: e.path,
5449
- score: score * 1e4 + hit,
4900
+ score: 3e4 + fuzzy,
5450
4901
  mtimeMs: e.mtimeMs,
5451
4902
  recent: recent.has(e.path)
5452
4903
  });
@@ -5458,29 +4909,63 @@ function rankPickerCandidates(files, query, limitOrOpts) {
5458
4909
  });
5459
4910
  return scored.slice(0, limit).map((s) => s.path);
5460
4911
  }
4912
+ function fuzzySubseqScore(needle, target) {
4913
+ if (needle.length === 0) return 0;
4914
+ const slashIdx = target.lastIndexOf("/");
4915
+ const basenameStart = slashIdx >= 0 ? slashIdx + 1 : 0;
4916
+ let qi = 0;
4917
+ let lastMatchIdx = -2;
4918
+ let consecutive = 0;
4919
+ let basenameMatches = 0;
4920
+ let totalGap = 0;
4921
+ for (let ti = 0; ti < target.length && qi < needle.length; ti++) {
4922
+ if (target[ti] !== needle[qi]) continue;
4923
+ if (ti === lastMatchIdx + 1) consecutive++;
4924
+ else if (lastMatchIdx >= 0) totalGap += ti - lastMatchIdx - 1;
4925
+ if (ti >= basenameStart) basenameMatches++;
4926
+ lastMatchIdx = ti;
4927
+ qi++;
4928
+ }
4929
+ if (qi < needle.length) return null;
4930
+ const quality = Math.max(0, totalGap - consecutive * 10 - basenameMatches * 5);
4931
+ const lengthPenalty = Math.floor(target.length / 4);
4932
+ return quality + lengthPenalty;
4933
+ }
5461
4934
  var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
5462
4935
  function expandAtMentions(text, rootDir, opts = {}) {
5463
4936
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
5464
- const fs4 = opts.fs ?? defaultFs;
4937
+ const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
4938
+ const fs5 = opts.fs ?? defaultFs;
5465
4939
  const root = resolve(rootDir);
5466
4940
  const seen = /* @__PURE__ */ new Map();
5467
4941
  const expansions = [];
4942
+ const dirListings = /* @__PURE__ */ new Map();
5468
4943
  for (const match of text.matchAll(AT_MENTION_PATTERN)) {
5469
4944
  const rawPath = match[1] ?? "";
5470
4945
  let cleaned = rawPath;
5471
4946
  while (cleaned.endsWith(".")) cleaned = cleaned.slice(0, -1);
4947
+ if (cleaned.endsWith("/") || cleaned.endsWith("\\")) cleaned = cleaned.slice(0, -1);
5472
4948
  if (!cleaned) continue;
5473
4949
  const token = `@${cleaned}`;
5474
4950
  if (seen.has(token)) continue;
5475
- const expansion = resolveMention(cleaned, root, maxBytes, fs4);
4951
+ const expansion = resolveMention(cleaned, root, maxBytes, maxDirEntries, fs5, dirListings);
5476
4952
  seen.set(token, expansion);
5477
4953
  expansions.push(expansion);
5478
4954
  }
5479
4955
  if (expansions.length === 0) return { text, expansions };
5480
4956
  const blocks = [];
5481
4957
  for (const ex of expansions) {
5482
- if (ex.ok) {
5483
- const content = readSafe(root, ex.path, fs4);
4958
+ if (ex.ok && ex.isDirectory) {
4959
+ const files = dirListings.get(ex.path) ?? [];
4960
+ const truncAttr = ex.truncated ? ' truncated="true"' : "";
4961
+ const body = files.length > 0 ? `
4962
+ ${files.join("\n")}
4963
+ ` : "\n";
4964
+ blocks.push(
4965
+ `<directory path="${ex.path}" entries="${ex.entries ?? files.length}"${truncAttr}>${body}</directory>`
4966
+ );
4967
+ } else if (ex.ok) {
4968
+ const content = readSafe(root, ex.path, fs5);
5484
4969
  blocks.push(`<file path="${ex.path}">
5485
4970
  ${content}
5486
4971
  </file>`);
@@ -5494,7 +4979,7 @@ ${content}
5494
4979
  ${blocks.join("\n\n")}`;
5495
4980
  return { text: augmented, expansions };
5496
4981
  }
5497
- function resolveMention(rawPath, root, maxBytes, fs4) {
4982
+ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings) {
5498
4983
  if (isAbsolute(rawPath)) {
5499
4984
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
5500
4985
  }
@@ -5503,22 +4988,34 @@ function resolveMention(rawPath, root, maxBytes, fs4) {
5503
4988
  if (rel.startsWith("..") || isAbsolute(rel)) {
5504
4989
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
5505
4990
  }
5506
- if (!fs4.exists(resolved)) {
4991
+ if (!fs5.exists(resolved)) {
5507
4992
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
5508
4993
  }
5509
- if (!fs4.isFile(resolved)) {
5510
- return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
4994
+ if (fs5.isFile(resolved)) {
4995
+ const size = fs5.size(resolved);
4996
+ if (size > maxBytes) {
4997
+ return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
4998
+ }
4999
+ return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
5511
5000
  }
5512
- const size = fs4.size(resolved);
5513
- if (size > maxBytes) {
5514
- return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
5001
+ if (fs5.isDir?.(resolved) && fs5.listDir) {
5002
+ const { files, truncated } = fs5.listDir(resolved, root, maxDirEntries);
5003
+ dirListings.set(rawPath, files);
5004
+ return {
5005
+ token: `@${rawPath}`,
5006
+ path: rawPath,
5007
+ ok: true,
5008
+ isDirectory: true,
5009
+ entries: files.length,
5010
+ truncated
5011
+ };
5515
5012
  }
5516
- return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
5013
+ return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
5517
5014
  }
5518
- function readSafe(root, rawPath, fs4) {
5015
+ function readSafe(root, rawPath, fs5) {
5519
5016
  const resolved = resolve(root, rawPath);
5520
5017
  try {
5521
- return fs4.read(resolved);
5018
+ return fs5.read(resolved);
5522
5019
  } catch {
5523
5020
  return "(read failed)";
5524
5021
  }
@@ -5532,6 +5029,24 @@ var defaultFs = {
5532
5029
  return false;
5533
5030
  }
5534
5031
  },
5032
+ isDir: (p) => {
5033
+ try {
5034
+ return statSync2(p).isDirectory();
5035
+ } catch {
5036
+ return false;
5037
+ }
5038
+ },
5039
+ listDir: (dirAbs, root, max) => {
5040
+ const dirRel = relative(root, dirAbs).split(/[\\/]/).join("/");
5041
+ const walkCap = Math.max(max * 4, 5e3);
5042
+ const all = listFilesSync(root, { maxResults: walkCap });
5043
+ const prefix = dirRel ? `${dirRel}/` : "";
5044
+ const filtered = dirRel ? all.filter((f) => f === dirRel || f.startsWith(prefix)) : all;
5045
+ return {
5046
+ files: filtered.slice(0, max),
5047
+ truncated: filtered.length > max
5048
+ };
5049
+ },
5535
5050
  size: (p) => {
5536
5051
  try {
5537
5052
  return statSync2(p).size;
@@ -6313,9 +5828,9 @@ function applyMemoryStack(basePrompt, rootDir) {
6313
5828
  }
6314
5829
 
6315
5830
  // src/tools/filesystem.ts
6316
- import { promises as fs3 } from "fs";
6317
- import * as pathMod3 from "path";
6318
- import picomatch2 from "picomatch";
5831
+ import { promises as fs4 } from "fs";
5832
+ import * as pathMod4 from "path";
5833
+ import picomatch3 from "picomatch";
6319
5834
 
6320
5835
  // src/tools/fs/edit.ts
6321
5836
  import { promises as fs } from "fs";
@@ -6350,6 +5865,83 @@ async function applyEdit(rootDir, abs, args) {
6350
5865
  return `${header}
6351
5866
  ${diff}`;
6352
5867
  }
5868
+ async function applyMultiEdit(rootDir, edits) {
5869
+ if (edits.length === 0) {
5870
+ throw new Error("multi_edit: edits must contain at least one entry");
5871
+ }
5872
+ const filesByPath = /* @__PURE__ */ new Map();
5873
+ for (let i = 0; i < edits.length; i++) {
5874
+ const e = edits[i];
5875
+ if (typeof e.abs !== "string" || e.abs.length === 0) {
5876
+ throw new Error(`multi_edit: edit #${i + 1} requires a string \`path\` (no edits applied)`);
5877
+ }
5878
+ if (typeof e.search !== "string") {
5879
+ throw new Error(`multi_edit: edit #${i + 1} requires a string \`search\` (no edits applied)`);
5880
+ }
5881
+ if (typeof e.replace !== "string") {
5882
+ throw new Error(
5883
+ `multi_edit: edit #${i + 1} requires a string \`replace\` (no edits applied)`
5884
+ );
5885
+ }
5886
+ const rel = displayRel(rootDir, e.abs);
5887
+ if (e.search.length === 0) {
5888
+ throw new Error(
5889
+ `multi_edit: edit #${i + 1} (${rel}) search cannot be empty (no edits applied)`
5890
+ );
5891
+ }
5892
+ let state = filesByPath.get(e.abs);
5893
+ if (!state) {
5894
+ let before;
5895
+ try {
5896
+ before = await fs.readFile(e.abs, "utf8");
5897
+ } catch (err) {
5898
+ throw new Error(
5899
+ `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
5900
+ );
5901
+ }
5902
+ const le = before.includes("\r\n") ? "\r\n" : "\n";
5903
+ state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
5904
+ filesByPath.set(e.abs, state);
5905
+ }
5906
+ const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
5907
+ const adaptedReplace = e.replace.replace(/\r?\n/g, state.le);
5908
+ const firstIdx = state.buf.indexOf(adaptedSearch);
5909
+ if (firstIdx < 0) {
5910
+ throw new Error(
5911
+ `multi_edit: edit #${i + 1} search text not found in ${rel} \u2014 no edits applied (multi_edit is atomic)`
5912
+ );
5913
+ }
5914
+ const nextIdx = state.buf.indexOf(adaptedSearch, firstIdx + 1);
5915
+ if (nextIdx >= 0) {
5916
+ throw new Error(
5917
+ `multi_edit: edit #${i + 1} search text appears multiple times in ${rel} \u2014 include more context to disambiguate (no edits applied)`
5918
+ );
5919
+ }
5920
+ const startLine = state.buf.slice(0, firstIdx).split(/\r?\n/).length;
5921
+ state.buf = state.buf.slice(0, firstIdx) + adaptedReplace + state.buf.slice(firstIdx + adaptedSearch.length);
5922
+ state.hunks.push(`# ${rel}
5923
+ ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
5924
+ state.deltaChars += adaptedReplace.length - adaptedSearch.length;
5925
+ state.touched++;
5926
+ }
5927
+ for (const [abs, state] of filesByPath) {
5928
+ await fs.writeFile(abs, state.buf, "utf8");
5929
+ }
5930
+ const fileCount = filesByPath.size;
5931
+ const editCount = edits.length;
5932
+ let totalDelta = 0;
5933
+ const allHunks = [];
5934
+ for (const state of filesByPath.values()) {
5935
+ totalDelta += state.deltaChars;
5936
+ allHunks.push(...state.hunks);
5937
+ }
5938
+ const sign = totalDelta >= 0 ? "+" : "";
5939
+ const editNoun = editCount === 1 ? "edit" : "edits";
5940
+ const fileNoun = fileCount === 1 ? "file" : "files";
5941
+ const header = `multi_edit: applied ${editCount} ${editNoun} across ${fileCount} ${fileNoun} (${sign}${totalDelta} chars)`;
5942
+ return `${header}
5943
+ ${allHunks.join("\n")}`;
5944
+ }
6353
5945
  function renderEditDiff(search, replace, startLine) {
6354
5946
  const a = search.split(/\r?\n/);
6355
5947
  const b = replace.split(/\r?\n/);
@@ -6396,15 +5988,78 @@ function lineDiff(a, b) {
6396
5988
  return out;
6397
5989
  }
6398
5990
 
6399
- // src/tools/fs/search.ts
5991
+ // src/tools/fs/glob.ts
6400
5992
  import { promises as fs2 } from "fs";
6401
5993
  import * as pathMod2 from "path";
5994
+ import picomatch2 from "picomatch";
5995
+ function displayRel2(rootDir, full) {
5996
+ return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
5997
+ }
5998
+ async function globFiles(ctx, startAbs, args) {
5999
+ if (args.signal?.aborted) {
6000
+ throw new DOMException("glob aborted by user", "AbortError");
6001
+ }
6002
+ const includeDeps = args.include_deps === true;
6003
+ const sortBy = args.sort_by ?? "mtime";
6004
+ const limit = Math.max(1, Math.min(1e3, Math.floor(args.limit ?? 200)));
6005
+ const isMatch = picomatch2(args.pattern, { dot: true, nocase: true });
6006
+ const hits = [];
6007
+ const walk2 = async (dir) => {
6008
+ if (args.signal?.aborted) {
6009
+ throw new DOMException("glob aborted by user", "AbortError");
6010
+ }
6011
+ let entries;
6012
+ try {
6013
+ entries = await fs2.readdir(dir, { withFileTypes: true });
6014
+ } catch {
6015
+ return;
6016
+ }
6017
+ for (const e of entries) {
6018
+ const full = pathMod2.join(dir, e.name);
6019
+ if (e.isDirectory()) {
6020
+ if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
6021
+ await walk2(full);
6022
+ continue;
6023
+ }
6024
+ if (!e.isFile() && !e.isSymbolicLink()) continue;
6025
+ const rel = displayRel2(ctx.rootDir, full);
6026
+ if (!isMatch(rel)) continue;
6027
+ let mtimeMs = 0;
6028
+ if (sortBy === "mtime") {
6029
+ try {
6030
+ const st = await fs2.stat(full);
6031
+ mtimeMs = st.mtimeMs;
6032
+ } catch {
6033
+ continue;
6034
+ }
6035
+ }
6036
+ hits.push({ rel, mtimeMs });
6037
+ }
6038
+ };
6039
+ await walk2(startAbs);
6040
+ if (hits.length === 0) return "(no matches)";
6041
+ if (sortBy === "mtime") hits.sort((a, b) => b.mtimeMs - a.mtimeMs);
6042
+ else hits.sort((a, b) => a.rel.localeCompare(b.rel));
6043
+ const truncated = hits.length > limit;
6044
+ const shown = hits.slice(0, limit);
6045
+ const lines = shown.map((h) => h.rel);
6046
+ if (truncated) {
6047
+ lines.push(
6048
+ `[\u2026 ${hits.length - limit} more matches \u2014 refine pattern or raise limit (max 1000) \u2026]`
6049
+ );
6050
+ }
6051
+ return lines.join("\n");
6052
+ }
6053
+
6054
+ // src/tools/fs/search.ts
6055
+ import { promises as fs3 } from "fs";
6056
+ import * as pathMod3 from "path";
6402
6057
  function throwIfAborted(signal) {
6403
6058
  if (!signal?.aborted) return;
6404
6059
  throw new DOMException("search aborted by user", "AbortError");
6405
6060
  }
6406
- function displayRel2(rootDir, full) {
6407
- return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
6061
+ function displayRel3(rootDir, full) {
6062
+ return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
6408
6063
  }
6409
6064
  async function searchFiles(ctx, startAbs, args) {
6410
6065
  throwIfAborted(args.signal);
@@ -6422,17 +6077,17 @@ async function searchFiles(ctx, startAbs, args) {
6422
6077
  throwIfAborted(args.signal);
6423
6078
  let entries;
6424
6079
  try {
6425
- entries = await fs2.readdir(dir, { withFileTypes: true });
6080
+ entries = await fs3.readdir(dir, { withFileTypes: true });
6426
6081
  } catch {
6427
6082
  return;
6428
6083
  }
6429
6084
  for (const e of entries) {
6430
6085
  throwIfAborted(args.signal);
6431
- const full = pathMod2.join(dir, e.name);
6086
+ const full = pathMod3.join(dir, e.name);
6432
6087
  const lower = e.name.toLowerCase();
6433
6088
  const hit = re ? re.test(e.name) : lower.includes(needle);
6434
6089
  if (hit) {
6435
- const rel = displayRel2(ctx.rootDir, full);
6090
+ const rel = displayRel3(ctx.rootDir, full);
6436
6091
  if (totalBytes + rel.length + 1 > ctx.maxListBytes) {
6437
6092
  matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
6438
6093
  return;
@@ -6453,6 +6108,7 @@ async function searchContent(ctx, startAbs, args) {
6453
6108
  throwIfAborted(args.signal);
6454
6109
  const caseSensitive = args.case_sensitive === true;
6455
6110
  const includeDeps = args.include_deps === true;
6111
+ const ctxLines = Math.max(0, Math.min(20, Math.floor(args.context ?? 0)));
6456
6112
  let re = null;
6457
6113
  try {
6458
6114
  re = new RegExp(args.pattern, caseSensitive ? "" : "i");
@@ -6464,12 +6120,22 @@ async function searchContent(ctx, startAbs, args) {
6464
6120
  let totalBytes = 0;
6465
6121
  let scanned = 0;
6466
6122
  let truncated = false;
6123
+ const pushLine = (out) => {
6124
+ if (totalBytes + out.length + 1 > ctx.maxListBytes) {
6125
+ matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
6126
+ truncated = true;
6127
+ return false;
6128
+ }
6129
+ matches.push(out);
6130
+ totalBytes += out.length + 1;
6131
+ return true;
6132
+ };
6467
6133
  const walk2 = async (dir) => {
6468
6134
  if (truncated) return;
6469
6135
  throwIfAborted(args.signal);
6470
6136
  let entries;
6471
6137
  try {
6472
- entries = await fs2.readdir(dir, { withFileTypes: true });
6138
+ entries = await fs3.readdir(dir, { withFileTypes: true });
6473
6139
  } catch {
6474
6140
  return;
6475
6141
  }
@@ -6478,16 +6144,16 @@ async function searchContent(ctx, startAbs, args) {
6478
6144
  throwIfAborted(args.signal);
6479
6145
  if (e.isDirectory()) {
6480
6146
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
6481
- await walk2(pathMod2.join(dir, e.name));
6147
+ await walk2(pathMod3.join(dir, e.name));
6482
6148
  continue;
6483
6149
  }
6484
6150
  if (!e.isFile()) continue;
6485
- const full = pathMod2.join(dir, e.name);
6486
- if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel2(ctx.rootDir, full))) continue;
6151
+ const full = pathMod3.join(dir, e.name);
6152
+ if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel3(ctx.rootDir, full))) continue;
6487
6153
  if (ctx.isBinaryByName(e.name)) continue;
6488
6154
  let fh;
6489
6155
  try {
6490
- fh = await fs2.open(full, "r");
6156
+ fh = await fs3.open(full, "r");
6491
6157
  } catch {
6492
6158
  continue;
6493
6159
  }
@@ -6510,25 +6176,45 @@ async function searchContent(ctx, startAbs, args) {
6510
6176
  const firstNul = raw.indexOf(0);
6511
6177
  if (firstNul !== -1 && firstNul < 8 * 1024) continue;
6512
6178
  const text = raw.toString("utf8");
6513
- const rel = displayRel2(ctx.rootDir, full);
6179
+ const rel = displayRel3(ctx.rootDir, full);
6514
6180
  const lines = text.split(/\r?\n/);
6181
+ const hits = [];
6515
6182
  for (let li = 0; li < lines.length; li++) {
6516
6183
  throwIfAborted(args.signal);
6517
6184
  const line = lines[li];
6518
6185
  const lineForCheck = caseSensitive ? line : line.toLowerCase();
6519
6186
  const hit = re ? re.test(line) : lineForCheck.includes(needle);
6520
- if (!hit) continue;
6521
- const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
6522
- const out = `${rel}:${li + 1}: ${display}`;
6523
- if (totalBytes + out.length + 1 > ctx.maxListBytes) {
6524
- matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
6525
- truncated = true;
6526
- return;
6527
- }
6528
- matches.push(out);
6529
- totalBytes += out.length + 1;
6187
+ if (hit) hits.push(li);
6530
6188
  }
6531
6189
  scanned++;
6190
+ if (hits.length === 0) continue;
6191
+ if (ctxLines === 0) {
6192
+ for (const li of hits) {
6193
+ if (truncated) return;
6194
+ const line = lines[li];
6195
+ const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
6196
+ if (!pushLine(`${rel}:${li + 1}: ${display}`)) return;
6197
+ }
6198
+ continue;
6199
+ }
6200
+ const hitSet = new Set(hits);
6201
+ let prevWindowEnd = -2;
6202
+ for (const li of hits) {
6203
+ if (truncated) return;
6204
+ const winStart = Math.max(0, li - ctxLines);
6205
+ const winEnd = Math.min(lines.length - 1, li + ctxLines);
6206
+ if (winStart > prevWindowEnd + 1 && prevWindowEnd >= 0) {
6207
+ if (!pushLine("--")) return;
6208
+ }
6209
+ const realStart = winStart > prevWindowEnd + 1 ? winStart : prevWindowEnd + 1;
6210
+ for (let i = realStart; i <= winEnd; i++) {
6211
+ const line = lines[i];
6212
+ const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
6213
+ const sep2 = hitSet.has(i) ? ":" : "-";
6214
+ if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
6215
+ }
6216
+ prevWindowEnd = winEnd;
6217
+ }
6532
6218
  }
6533
6219
  };
6534
6220
  await walk2(startAbs);
@@ -6546,8 +6232,8 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
6546
6232
  var AUTO_PREVIEW_TAIL_LINES = 40;
6547
6233
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
6548
6234
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
6549
- function displayRel3(rootDir, full) {
6550
- return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
6235
+ function displayRel4(rootDir, full) {
6236
+ return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
6551
6237
  }
6552
6238
  var GLOB_METACHARS = /[*?{[]/;
6553
6239
  function compileNameFilter(filter) {
@@ -6557,7 +6243,7 @@ function compileNameFilter(filter) {
6557
6243
  return (name) => name.toLowerCase().includes(needle);
6558
6244
  }
6559
6245
  const matchPath = filter.includes("/");
6560
- const isMatch = picomatch2(filter, { dot: true, nocase: true });
6246
+ const isMatch = picomatch3(filter, { dot: true, nocase: true });
6561
6247
  return matchPath ? (_n, rel) => isMatch(rel) : (name) => isMatch(name);
6562
6248
  }
6563
6249
  function isLikelyBinaryByName(name) {
@@ -6566,7 +6252,7 @@ function isLikelyBinaryByName(name) {
6566
6252
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
6567
6253
  }
6568
6254
  function registerFilesystemTools(registry, opts) {
6569
- const rootDir = pathMod3.resolve(opts.rootDir);
6255
+ const rootDir = pathMod4.resolve(opts.rootDir);
6570
6256
  const allowWriting = opts.allowWriting !== false;
6571
6257
  const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
6572
6258
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
@@ -6579,10 +6265,10 @@ function registerFilesystemTools(registry, opts) {
6579
6265
  normalized = normalized.slice(1);
6580
6266
  }
6581
6267
  if (normalized.length === 0) normalized = ".";
6582
- const resolved = pathMod3.resolve(rootDir, normalized);
6583
- const normRoot = pathMod3.resolve(rootDir);
6584
- const rel = pathMod3.relative(normRoot, resolved);
6585
- if (rel.startsWith("..") || pathMod3.isAbsolute(rel)) {
6268
+ const resolved = pathMod4.resolve(rootDir, normalized);
6269
+ const normRoot = pathMod4.resolve(rootDir);
6270
+ const rel = pathMod4.relative(normRoot, resolved);
6271
+ if (rel.startsWith("..") || pathMod4.isAbsolute(rel)) {
6586
6272
  throw new Error(
6587
6273
  `path escapes sandbox root (${normRoot}): ${raw} \u2014 workspace is pinned at launch; quit and relaunch with \`reasonix code --dir <path>\` to work in a different folder`
6588
6274
  );
@@ -6614,7 +6300,7 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
6614
6300
  },
6615
6301
  fn: async (args) => {
6616
6302
  const abs = safePath(args.path);
6617
- const fh = await fs3.open(abs, "r");
6303
+ const fh = await fs4.open(abs, "r");
6618
6304
  let raw;
6619
6305
  try {
6620
6306
  const stat2 = await fh.stat();
@@ -6688,7 +6374,7 @@ ${slice.join("\n")}`;
6688
6374
  },
6689
6375
  fn: async (args) => {
6690
6376
  const abs = safePath(args.path ?? ".");
6691
- const entries = await fs3.readdir(abs, { withFileTypes: true });
6377
+ const entries = await fs4.readdir(abs, { withFileTypes: true });
6692
6378
  const lines = [];
6693
6379
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
6694
6380
  lines.push(e.isDirectory() ? `${e.name}/` : e.name);
@@ -6732,7 +6418,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6732
6418
  if (depth > maxDepth) return;
6733
6419
  let entries;
6734
6420
  try {
6735
- entries = await fs3.readdir(dir, { withFileTypes: true });
6421
+ entries = await fs4.readdir(dir, { withFileTypes: true });
6736
6422
  } catch {
6737
6423
  return;
6738
6424
  }
@@ -6767,7 +6453,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6767
6453
  lines.push(line);
6768
6454
  emitted++;
6769
6455
  if (e.isDirectory() && !skip) {
6770
- await walk2(pathMod3.join(dir, e.name), depth + 1);
6456
+ await walk2(pathMod4.join(dir, e.name), depth + 1);
6771
6457
  }
6772
6458
  }
6773
6459
  };
@@ -6828,6 +6514,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6828
6514
  include_deps: {
6829
6515
  type: "boolean",
6830
6516
  description: "When true, also search inside node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
6517
+ },
6518
+ context: {
6519
+ type: "integer",
6520
+ 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."
6831
6521
  }
6832
6522
  },
6833
6523
  required: ["pattern"]
@@ -6844,6 +6534,43 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6844
6534
  { ...args, signal: toolCtx?.signal }
6845
6535
  )
6846
6536
  });
6537
+ registry.register({
6538
+ name: "glob",
6539
+ parallelSafe: true,
6540
+ description: "List files matching a glob pattern, sorted by mtime (most-recently-modified first) by default. Use this for 'what changed lately', 'find all *.test.ts', 'all configs under src/'. Glob syntax matches the cross-tool standard: `*` (any chars in one segment), `**` (any segments), `?` (one char), `{a,b}` (alternation). Pattern matches against the path RELATIVE to the search root (e.g. 'src/**/*.ts' from project root). Skips node_modules / .git / dist / build / etc by default. Default limit 200; raise via `limit` (max 1000). Different from `search_files` (substring on basename) and `search_content` (matches inside file contents).",
6541
+ readOnly: true,
6542
+ parameters: {
6543
+ type: "object",
6544
+ properties: {
6545
+ pattern: {
6546
+ type: "string",
6547
+ description: "Glob pattern, e.g. 'src/**/*.ts', '**/*.{md,mdx}', 'tests/*.test.ts'."
6548
+ },
6549
+ path: {
6550
+ type: "string",
6551
+ description: "Base directory to walk (default: sandbox root). The pattern matches relative to this path."
6552
+ },
6553
+ sort_by: {
6554
+ type: "string",
6555
+ enum: ["mtime", "name"],
6556
+ description: "Sort order. 'mtime' (default) shows most-recently-modified first \u2014 useful for 'what did I change today'. 'name' is alphabetical."
6557
+ },
6558
+ include_deps: {
6559
+ type: "boolean",
6560
+ description: "When true, also walk node_modules / .git / dist / build / etc. Off by default."
6561
+ },
6562
+ limit: {
6563
+ type: "integer",
6564
+ description: "Cap on returned matches. Default 200; clamped to [1, 1000]."
6565
+ }
6566
+ },
6567
+ required: ["pattern"]
6568
+ },
6569
+ fn: async (args, toolCtx) => globFiles({ rootDir, skipDirNames: SKIP_DIR_NAMES }, safePath(args.path ?? "."), {
6570
+ ...args,
6571
+ signal: toolCtx?.signal
6572
+ })
6573
+ });
6847
6574
  registry.register({
6848
6575
  name: "get_file_info",
6849
6576
  parallelSafe: true,
@@ -6858,7 +6585,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6858
6585
  },
6859
6586
  fn: async (args) => {
6860
6587
  const abs = safePath(args.path);
6861
- const st = await fs3.lstat(abs);
6588
+ const st = await fs4.lstat(abs);
6862
6589
  const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
6863
6590
  return JSON.stringify({
6864
6591
  type,
@@ -6881,9 +6608,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6881
6608
  },
6882
6609
  fn: async (args) => {
6883
6610
  const abs = safePath(args.path);
6884
- await fs3.mkdir(pathMod3.dirname(abs), { recursive: true });
6885
- await fs3.writeFile(abs, args.content, "utf8");
6886
- return `wrote ${args.content.length} chars to ${displayRel3(rootDir, abs)}`;
6611
+ await fs4.mkdir(pathMod4.dirname(abs), { recursive: true });
6612
+ await fs4.writeFile(abs, args.content, "utf8");
6613
+ return `wrote ${args.content.length} chars to ${displayRel4(rootDir, abs)}`;
6887
6614
  }
6888
6615
  });
6889
6616
  registry.register({
@@ -6900,6 +6627,43 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6900
6627
  },
6901
6628
  fn: async (args) => applyEdit(rootDir, safePath(args.path), args)
6902
6629
  });
6630
+ registry.register({
6631
+ name: "multi_edit",
6632
+ description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in a single atomic call. Edits run sequentially in array order; for edits that touch the same file, a later edit can match text inserted by an earlier one. If ANY edit fails (search not found, ambiguous match, empty search, file unreadable), NO files are written \u2014 atomic at the validation layer. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
6633
+ parameters: {
6634
+ type: "object",
6635
+ properties: {
6636
+ edits: {
6637
+ type: "array",
6638
+ description: "Edits to apply in order. Length \u2265 1. Each edit names its own target file.",
6639
+ items: {
6640
+ type: "object",
6641
+ properties: {
6642
+ path: {
6643
+ type: "string",
6644
+ description: "File the edit targets (sandbox-relative or absolute)."
6645
+ },
6646
+ search: {
6647
+ type: "string",
6648
+ description: "Exact text to find (must be unique in the file)."
6649
+ },
6650
+ replace: { type: "string", description: "Text to substitute in place of `search`." }
6651
+ },
6652
+ required: ["path", "search", "replace"]
6653
+ }
6654
+ }
6655
+ },
6656
+ required: ["edits"]
6657
+ },
6658
+ fn: async (args) => {
6659
+ const resolved = (args.edits ?? []).map((e) => ({
6660
+ abs: safePath(e?.path),
6661
+ search: e?.search,
6662
+ replace: e?.replace
6663
+ }));
6664
+ return applyMultiEdit(rootDir, resolved);
6665
+ }
6666
+ });
6903
6667
  registry.register({
6904
6668
  name: "create_directory",
6905
6669
  description: "Create a directory (and any missing parents) under the sandbox root.",
@@ -6910,8 +6674,8 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6910
6674
  },
6911
6675
  fn: async (args) => {
6912
6676
  const abs = safePath(args.path);
6913
- await fs3.mkdir(abs, { recursive: true });
6914
- return `created ${displayRel3(rootDir, abs)}/`;
6677
+ await fs4.mkdir(abs, { recursive: true });
6678
+ return `created ${displayRel4(rootDir, abs)}/`;
6915
6679
  }
6916
6680
  });
6917
6681
  registry.register({
@@ -6928,9 +6692,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
6928
6692
  fn: async (args) => {
6929
6693
  const src = safePath(args.source);
6930
6694
  const dst = safePath(args.destination);
6931
- await fs3.mkdir(pathMod3.dirname(dst), { recursive: true });
6932
- await fs3.rename(src, dst);
6933
- return `moved ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
6695
+ await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
6696
+ await fs4.rename(src, dst);
6697
+ return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
6934
6698
  }
6935
6699
  });
6936
6700
  return registry;
@@ -7409,6 +7173,108 @@ function registerPlanTool(registry, opts = {}) {
7409
7173
  return registry;
7410
7174
  }
7411
7175
 
7176
+ // src/tools/todo.ts
7177
+ var DESCRIPTION = 'In-session task tracker for multi-step work. NOT a plan \u2014 no approval gate, no checkpoint pauses, doesn\'t touch any files. The tool replaces the entire todo list every call (set semantics, NOT append). Pass the FULL list every time.\n\nWhen to use:\n\u2022 The task has 3+ distinct steps and you want to keep them straight as you work.\n\u2022 The user gave you a multi-part request ("do A, then B, then C").\n\u2022 You\'re partway through a long task and want to record where you are so a future you doesn\'t lose the thread.\n\nWhen NOT to use:\n\u2022 One-shot edits, single-question answers, single-tool tasks.\n\u2022 User-facing approval gates \u2192 that\'s `submit_plan`.\n\u2022 Branching choices \u2192 that\'s `ask_choice`.\n\nRules:\n\u2022 Exactly ONE todo may have status:"in_progress" at a time (or zero \u2014 between steps).\n\u2022 Mark a todo "completed" the moment it\'s actually done \u2014 don\'t batch.\n\u2022 Each todo: `content` (imperative, e.g. "Add tests"), `activeForm` (gerund shown while running, e.g. "Adding tests"), `status`.\n\u2022 Empty `todos:[]` is allowed \u2014 it clears the list when work is fully done.';
7178
+ function validateTodos(raw) {
7179
+ if (!Array.isArray(raw)) {
7180
+ throw new Error("todo_write: `todos` must be an array");
7181
+ }
7182
+ const out = [];
7183
+ let inProgressCount = 0;
7184
+ for (let i = 0; i < raw.length; i++) {
7185
+ const entry = raw[i];
7186
+ if (!entry || typeof entry !== "object") {
7187
+ throw new Error(`todo_write: todo #${i + 1} must be an object`);
7188
+ }
7189
+ const e = entry;
7190
+ const content = typeof e.content === "string" ? e.content.trim() : "";
7191
+ const activeForm = typeof e.activeForm === "string" ? e.activeForm.trim() : "";
7192
+ const status = e.status;
7193
+ if (!content) {
7194
+ throw new Error(`todo_write: todo #${i + 1} \`content\` must be a non-empty string`);
7195
+ }
7196
+ if (!activeForm) {
7197
+ throw new Error(`todo_write: todo #${i + 1} \`activeForm\` must be a non-empty string`);
7198
+ }
7199
+ if (status !== "pending" && status !== "in_progress" && status !== "completed") {
7200
+ throw new Error(
7201
+ `todo_write: todo #${i + 1} \`status\` must be one of pending|in_progress|completed (got ${JSON.stringify(status)})`
7202
+ );
7203
+ }
7204
+ if (status === "in_progress") {
7205
+ inProgressCount++;
7206
+ if (inProgressCount > 1) {
7207
+ throw new Error(
7208
+ "todo_write: at most one todo may be in_progress at a time \u2014 mark the previous one completed first"
7209
+ );
7210
+ }
7211
+ }
7212
+ out.push({ content, status, activeForm });
7213
+ }
7214
+ return out;
7215
+ }
7216
+ function renderTodos(todos) {
7217
+ if (todos.length === 0) return "todos cleared (0 items)";
7218
+ let done = 0;
7219
+ let inProgress = 0;
7220
+ let pending = 0;
7221
+ for (const t2 of todos) {
7222
+ if (t2.status === "completed") done++;
7223
+ else if (t2.status === "in_progress") inProgress++;
7224
+ else pending++;
7225
+ }
7226
+ const header = `todos updated \xB7 ${done} done \xB7 ${inProgress} in progress \xB7 ${pending} pending`;
7227
+ const lines = todos.map((t2) => {
7228
+ if (t2.status === "completed") return `[x] ${t2.content}`;
7229
+ if (t2.status === "in_progress") return `[>] ${t2.activeForm}`;
7230
+ return `[ ] ${t2.content}`;
7231
+ });
7232
+ return `${header}
7233
+ ${lines.join("\n")}`;
7234
+ }
7235
+ function registerTodoTool(registry, opts = {}) {
7236
+ registry.register({
7237
+ name: "todo_write",
7238
+ description: DESCRIPTION,
7239
+ readOnly: true,
7240
+ parameters: {
7241
+ type: "object",
7242
+ properties: {
7243
+ todos: {
7244
+ type: "array",
7245
+ description: "The COMPLETE new todo list. Replaces whatever was there before. Pass [] to clear.",
7246
+ items: {
7247
+ type: "object",
7248
+ properties: {
7249
+ content: {
7250
+ type: "string",
7251
+ description: 'Imperative step description, e.g. "Add tests for parser".'
7252
+ },
7253
+ status: {
7254
+ type: "string",
7255
+ enum: ["pending", "in_progress", "completed"],
7256
+ description: "Current state. Exactly one item may be in_progress."
7257
+ },
7258
+ activeForm: {
7259
+ type: "string",
7260
+ description: 'Gerund form shown while in_progress, e.g. "Adding tests for parser".'
7261
+ }
7262
+ },
7263
+ required: ["content", "status", "activeForm"]
7264
+ }
7265
+ }
7266
+ },
7267
+ required: ["todos"]
7268
+ },
7269
+ fn: async (args) => {
7270
+ const todos = validateTodos(args?.todos);
7271
+ opts.onTodosUpdated?.(todos);
7272
+ return renderTodos(todos);
7273
+ }
7274
+ });
7275
+ return registry;
7276
+ }
7277
+
7412
7278
  // src/tools/subagent-types.ts
7413
7279
  var EXPLORE_SYSTEM = `You are an exploration subagent. Wide-net read-only investigation; return one distilled answer.
7414
7280
 
@@ -7778,11 +7644,11 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
7778
7644
  }
7779
7645
 
7780
7646
  // src/tools/shell.ts
7781
- import * as pathMod7 from "path";
7647
+ import * as pathMod8 from "path";
7782
7648
 
7783
7649
  // src/tools/jobs.ts
7784
7650
  import { spawn as spawn2 } from "child_process";
7785
- import * as pathMod4 from "path";
7651
+ import * as pathMod5 from "path";
7786
7652
  function killProcessTree(pid, signal) {
7787
7653
  if (process.platform === "win32") {
7788
7654
  const args = ["/pid", String(pid), "/T"];
@@ -7842,7 +7708,7 @@ var JobRegistry = class {
7842
7708
  const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
7843
7709
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
7844
7710
  const spawnOpts = {
7845
- cwd: pathMod4.resolve(opts.cwd),
7711
+ cwd: pathMod5.resolve(opts.cwd),
7846
7712
  shell: false,
7847
7713
  windowsHide: true,
7848
7714
  env: process.env,
@@ -8128,12 +7994,12 @@ function latestOutputSince(before, after) {
8128
7994
  // src/tools/shell/exec.ts
8129
7995
  import { spawn as spawn4, spawnSync } from "child_process";
8130
7996
  import { existsSync as existsSync8, statSync as statSync4 } from "fs";
8131
- import * as pathMod6 from "path";
7997
+ import * as pathMod7 from "path";
8132
7998
 
8133
7999
  // src/tools/shell-chain.ts
8134
8000
  import { spawn as spawn3 } from "child_process";
8135
8001
  import { closeSync, openSync } from "fs";
8136
- import * as pathMod5 from "path";
8002
+ import * as pathMod6 from "path";
8137
8003
  var UnsupportedSyntaxError = class extends Error {
8138
8004
  constructor(detail) {
8139
8005
  super(`run_command: ${detail}`);
@@ -8400,7 +8266,7 @@ function openRedirects(redirects, cwd) {
8400
8266
  let bothFd = null;
8401
8267
  const toClose = [];
8402
8268
  const open = (target, flags) => {
8403
- const resolved = pathMod5.resolve(cwd, target);
8269
+ const resolved = pathMod6.resolve(cwd, target);
8404
8270
  const fd = openSync(resolved, flags);
8405
8271
  toClose.push(fd);
8406
8272
  return fd;
@@ -8896,16 +8762,16 @@ function resolveExecutable(cmd, opts = {}) {
8896
8762
  const platform = opts.platform ?? process.platform;
8897
8763
  if (platform !== "win32") return cmd;
8898
8764
  if (!cmd) return cmd;
8899
- if (cmd.includes("/") || cmd.includes("\\") || pathMod6.isAbsolute(cmd)) return cmd;
8900
- if (pathMod6.extname(cmd)) return cmd;
8765
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod7.isAbsolute(cmd)) return cmd;
8766
+ if (pathMod7.extname(cmd)) return cmd;
8901
8767
  const env = opts.env ?? process.env;
8902
8768
  const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
8903
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod6.delimiter);
8769
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod7.delimiter);
8904
8770
  const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
8905
8771
  const isFile = opts.isFile ?? defaultIsFile;
8906
8772
  for (const dir of pathDirs) {
8907
8773
  for (const ext of pathExt) {
8908
- const full = pathMod6.win32.join(dir, cmd + ext);
8774
+ const full = pathMod7.win32.join(dir, cmd + ext);
8909
8775
  if (isFile(full)) return full;
8910
8776
  }
8911
8777
  }
@@ -8975,8 +8841,8 @@ function withUtf8Codepage(cmdline) {
8975
8841
  function isBareWindowsName(s) {
8976
8842
  if (!s) return false;
8977
8843
  if (s.includes("/") || s.includes("\\")) return false;
8978
- if (pathMod6.isAbsolute(s)) return false;
8979
- if (pathMod6.extname(s)) return false;
8844
+ if (pathMod7.isAbsolute(s)) return false;
8845
+ if (pathMod7.extname(s)) return false;
8980
8846
  return true;
8981
8847
  }
8982
8848
  function quoteForCmdExe(arg) {
@@ -8997,7 +8863,7 @@ var NeedsConfirmationError = class extends Error {
8997
8863
  }
8998
8864
  };
8999
8865
  function registerShellTools(registry, opts) {
9000
- const rootDir = pathMod7.resolve(opts.rootDir);
8866
+ const rootDir = pathMod8.resolve(opts.rootDir);
9001
8867
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
9002
8868
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
9003
8869
  const jobs = opts.jobs ?? new JobRegistry();
@@ -9621,14 +9487,6 @@ function recordFromLoopEvent(ev, extra) {
9621
9487
  if (ev.toolName !== void 0) rec.tool = ev.toolName;
9622
9488
  if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
9623
9489
  if (ev.error !== void 0) rec.error = ev.error;
9624
- if (ev.planState && !isPlanStateEmptyShape(ev.planState)) {
9625
- rec.planState = {
9626
- subgoals: [...ev.planState.subgoals],
9627
- hypotheses: [...ev.planState.hypotheses],
9628
- uncertainties: [...ev.planState.uncertainties],
9629
- rejectedPaths: [...ev.planState.rejectedPaths]
9630
- };
9631
- }
9632
9490
  if (ev.stats) {
9633
9491
  rec.usage = {
9634
9492
  prompt_tokens: ev.stats.usage.promptTokens,
@@ -9664,9 +9522,6 @@ function readTranscript(path2) {
9664
9522
  const raw = readFileSync11(path2, "utf8");
9665
9523
  return parseTranscript(raw);
9666
9524
  }
9667
- function isPlanStateEmptyShape(s) {
9668
- return s.subgoals.length === 0 && s.hypotheses.length === 0 && s.uncertainties.length === 0 && s.rejectedPaths.length === 0;
9669
- }
9670
9525
  function parseTranscript(raw) {
9671
9526
  const out = { meta: null, records: [] };
9672
9527
  for (const line of raw.split(/\r?\n/)) {
@@ -9702,20 +9557,12 @@ function computeReplayStats(records) {
9702
9557
  const prefixHashes = /* @__PURE__ */ new Set();
9703
9558
  let userTurns = 0;
9704
9559
  let toolCalls = 0;
9705
- let harvestedTurns = 0;
9706
- let totalUncertainties = 0;
9707
- let totalSubgoals = 0;
9708
9560
  for (const rec of records) {
9709
9561
  if (rec.role === "user") userTurns++;
9710
9562
  else if (rec.role === "tool") toolCalls++;
9711
9563
  else if (rec.role === "assistant_final") {
9712
9564
  if (rec.model) models.add(rec.model);
9713
9565
  if (rec.prefixHash) prefixHashes.add(rec.prefixHash);
9714
- if (rec.planState) {
9715
- harvestedTurns++;
9716
- totalUncertainties += rec.planState.uncertainties.length;
9717
- totalSubgoals += rec.planState.subgoals.length;
9718
- }
9719
9566
  if (rec.usage && rec.model) {
9720
9567
  const u = new Usage(
9721
9568
  rec.usage.prompt_tokens ?? 0,
@@ -9743,9 +9590,6 @@ function computeReplayStats(records) {
9743
9590
  prefixHashes: [...prefixHashes],
9744
9591
  userTurns,
9745
9592
  toolCalls,
9746
- harvestedTurns,
9747
- totalUncertainties,
9748
- totalSubgoals,
9749
9593
  ...summarizeTurns(turns)
9750
9594
  };
9751
9595
  }
@@ -9923,41 +9767,6 @@ function renderSummaryTable(report, _opts = {}) {
9923
9767
  )
9924
9768
  );
9925
9769
  lines.push(statRow("prefix hashes", a.stats.prefixHashes.length, b.stats.prefixHashes.length));
9926
- if (a.stats.harvestedTurns > 0 || b.stats.harvestedTurns > 0) {
9927
- lines.push(
9928
- row(
9929
- [
9930
- "harvest turns",
9931
- `${a.stats.harvestedTurns}`,
9932
- `${b.stats.harvestedTurns}`,
9933
- signed(b.stats.harvestedTurns - a.stats.harvestedTurns)
9934
- ],
9935
- [20, 14, 14, 14]
9936
- )
9937
- );
9938
- lines.push(
9939
- row(
9940
- [
9941
- " subgoals",
9942
- `${a.stats.totalSubgoals}`,
9943
- `${b.stats.totalSubgoals}`,
9944
- signed(b.stats.totalSubgoals - a.stats.totalSubgoals)
9945
- ],
9946
- [20, 14, 14, 14]
9947
- )
9948
- );
9949
- lines.push(
9950
- row(
9951
- [
9952
- " uncertainties",
9953
- `${a.stats.totalUncertainties}`,
9954
- `${b.stats.totalUncertainties}`,
9955
- signed(b.stats.totalUncertainties - a.stats.totalUncertainties)
9956
- ],
9957
- [20, 14, 14, 14]
9958
- )
9959
- );
9960
- }
9961
9770
  lines.push("");
9962
9771
  const aPrefixStable = a.stats.prefixHashes.length <= 1;
9963
9772
  const bPrefixStable = b.stats.prefixHashes.length <= 1;
@@ -10029,17 +9838,6 @@ function renderMarkdown(report) {
10029
9838
  out.push(
10030
9839
  `| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
10031
9840
  );
10032
- if (a.stats.harvestedTurns > 0 || b.stats.harvestedTurns > 0) {
10033
- out.push(
10034
- `| harvest turns | ${a.stats.harvestedTurns} | ${b.stats.harvestedTurns} | ${signed(b.stats.harvestedTurns - a.stats.harvestedTurns)} |`
10035
- );
10036
- out.push(
10037
- `| harvest subgoals | ${a.stats.totalSubgoals} | ${b.stats.totalSubgoals} | ${signed(b.stats.totalSubgoals - a.stats.totalSubgoals)} |`
10038
- );
10039
- out.push(
10040
- `| harvest uncertainties | ${a.stats.totalUncertainties} | ${b.stats.totalUncertainties} | ${signed(b.stats.totalUncertainties - a.stats.totalUncertainties)} |`
10041
- );
10042
- }
10043
9841
  out.push("");
10044
9842
  out.push("## Turn-by-turn");
10045
9843
  out.push("");
@@ -10110,7 +9908,7 @@ function truncate(s, n) {
10110
9908
  // src/version.ts
10111
9909
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
10112
9910
  import { homedir as homedir6 } from "os";
10113
- import { dirname as dirname6, join as join11 } from "path";
9911
+ import { dirname as dirname6, join as join12 } from "path";
10114
9912
  import { fileURLToPath as fileURLToPath2 } from "url";
10115
9913
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
10116
9914
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -10119,7 +9917,7 @@ function readPackageVersion() {
10119
9917
  try {
10120
9918
  let dir = dirname6(fileURLToPath2(import.meta.url));
10121
9919
  for (let i = 0; i < 6; i++) {
10122
- const p = join11(dir, "package.json");
9920
+ const p = join12(dir, "package.json");
10123
9921
  if (existsSync9(p)) {
10124
9922
  const pkg = JSON.parse(readFileSync12(p, "utf8"));
10125
9923
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
@@ -10136,7 +9934,7 @@ function readPackageVersion() {
10136
9934
  }
10137
9935
  var VERSION = readPackageVersion();
10138
9936
  function cachePath(homeDirOverride) {
10139
- return join11(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
9937
+ return join12(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
10140
9938
  }
10141
9939
  function readCache(homeDirOverride) {
10142
9940
  try {
@@ -11130,8 +10928,8 @@ function lineEndingOf(text) {
11130
10928
 
11131
10929
  // src/code/prompt.ts
11132
10930
  import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
11133
- import { join as join12 } from "path";
11134
- var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, list_directory, directory_tree, search_files, search_content, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell.
10931
+ import { join as join13 } from "path";
10932
+ var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, multi_edit, list_directory, directory_tree, search_files, search_content, glob, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell, plus \`todo_write\` for in-session multi-step tracking.
11135
10933
 
11136
10934
  # Cite or shut up \u2014 non-negotiable
11137
10935
 
@@ -11179,10 +10977,23 @@ Skip it when one option is clearly correct (just do it, or submit_plan) or a fre
11179
10977
 
11180
10978
  Each option: short stable id (A/B/C), one-line title, optional summary. \`allowCustom: true\` when their real answer might not fit. Max 6. A ~1-sentence lead-in before the call is fine ("I see three directions \u2014 letting you pick"); don't repeat the options in it. After the call, STOP.
11181
10979
 
10980
+ # When to track multi-step intent (todo_write)
10981
+
10982
+ \`todo_write\` is a lightweight in-session task tracker \u2014 NOT a plan. No approval gate, no checkpoint pauses, doesn't touch files. Use it when the task has 3+ distinct steps and you'd otherwise lose track of where you are. Each call REPLACES the entire list (set semantics). Exactly one item may be \`in_progress\` at a time \u2014 flip it to \`completed\` the moment that step's done, before starting the next.
10983
+
10984
+ Use it for:
10985
+ - Multi-part user requests ("do A, then B, then C") \u2014 record the parts so you don't drop one.
10986
+ - Long refactors where you've finished step 2 of 5 and want a visible record.
10987
+ - Any moment where you'd otherwise enumerate "1. ... 2. ... 3. ..." in prose \u2014 the tool is strictly better, the UI shows progress live.
10988
+
10989
+ Skip it for: one-shot edits, single-question answers, anything that fits in one tool call. Don't \`todo_write\` and \`submit_plan\` for the same work \u2014 \`submit_plan\` is for tasks that need a review gate; \`todo_write\` is for personal bookkeeping after the user has already given you the green light.
10990
+
10991
+ Call shape: \`{ todos: [{ content, activeForm, status }, ...] }\` \u2014 \`content\` is imperative ("Add tests"), \`activeForm\` is gerund ("Adding tests") shown while \`in_progress\`. Pass the FULL list every call, not a delta. Pass \`todos: []\` to clear when work's done.
10992
+
11182
10993
  # Plan mode (/plan)
11183
10994
 
11184
10995
  The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
11185
- - Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
10996
+ - Write tools (edit_file, multi_edit, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
11186
10997
  - Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
11187
10998
  - You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
11188
10999
 
@@ -11251,6 +11062,7 @@ Rules:
11251
11062
  >>>>>>> REPLACE
11252
11063
  - Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
11253
11064
  - Paths are relative to the working directory. Don't use absolute paths.
11065
+ - For multi-site changes \u2014 same file or across files \u2014 prefer \`multi_edit\` over N \`edit_file\` calls. Shape: \`{ edits: [{ path, search, replace }, ...] }\`. All edits validate before any file is written; any failure \u2192 ALL files untouched. Per-file edits run in array order, so a later edit can match text inserted by an earlier one.
11254
11066
 
11255
11067
  # Trust what you already know
11256
11068
 
@@ -11260,7 +11072,7 @@ Before exploring the filesystem to answer a factual question, check whether the
11260
11072
 
11261
11073
  - Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
11262
11074
  - Prefer \`search_files\` over \`list_directory\` when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees. Note: \`search_files\` matches file NAMES; for searching file CONTENTS use \`search_content\`.
11263
- - Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
11075
+ - Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`glob\` (mtime-sorted glob \u2014 use for "what changed lately", "all *.ts under src/"), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"; pass \`context:N\` for grep -C N around hits), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
11264
11076
 
11265
11077
  # Path conventions
11266
11078
 
@@ -11333,7 +11145,7 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
11333
11145
  function codeSystemPrompt(rootDir, opts = {}) {
11334
11146
  const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
11335
11147
  const withMemory = applyMemoryStack(base, rootDir);
11336
- const gitignorePath = join12(rootDir, ".gitignore");
11148
+ const gitignorePath = join13(rootDir, ".gitignore");
11337
11149
  let result = withMemory;
11338
11150
  if (existsSync11(gitignorePath)) {
11339
11151
  let content;
@@ -11384,9 +11196,9 @@ import {
11384
11196
  writeFileSync as writeFileSync7
11385
11197
  } from "fs";
11386
11198
  import { homedir as homedir7 } from "os";
11387
- import { dirname as dirname8, join as join13 } from "path";
11199
+ import { dirname as dirname8, join as join14 } from "path";
11388
11200
  function defaultUsageLogPath(homeDirOverride) {
11389
- return join13(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
11201
+ return join14(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
11390
11202
  }
11391
11203
  var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
11392
11204
  var USAGE_RETENTION_DAYS = 365;
@@ -11588,6 +11400,7 @@ export {
11588
11400
  CODE_SYSTEM_PROMPT,
11589
11401
  CacheFirstLoop,
11590
11402
  ChoiceRequestedError,
11403
+ DEFAULT_AT_DIR_MAX_ENTRIES,
11591
11404
  DEFAULT_AT_MENTION_MAX_BYTES,
11592
11405
  DEFAULT_MAX_RESULT_CHARS,
11593
11406
  DEFAULT_MAX_RESULT_TOKENS,
@@ -11620,7 +11433,6 @@ export {
11620
11433
  Usage,
11621
11434
  VERSION,
11622
11435
  VolatileScratch,
11623
- aggregateBranchUsage,
11624
11436
  aggregateUsage,
11625
11437
  analyzeSchema,
11626
11438
  appendSessionMessage,
@@ -11640,13 +11452,11 @@ export {
11640
11452
  costUsd,
11641
11453
  decideOutcome,
11642
11454
  defaultConfigPath,
11643
- defaultSelector,
11644
11455
  defaultUsageLogPath,
11645
11456
  deleteSession,
11646
11457
  detectAtPicker,
11647
11458
  detectShellOperator,
11648
11459
  diffTranscripts,
11649
- emptyPlanState,
11650
11460
  expandAtMentions,
11651
11461
  fetchWithRetry,
11652
11462
  fixToolCallPairing,
@@ -11660,7 +11470,6 @@ export {
11660
11470
  formatSearchResults,
11661
11471
  getLatestVersion,
11662
11472
  globalSettingsPath,
11663
- harvest,
11664
11473
  healLoadedMessages,
11665
11474
  healLoadedMessagesByTokens,
11666
11475
  htmlToText,
@@ -11670,7 +11479,6 @@ export {
11670
11479
  isAllowed,
11671
11480
  isJsonRpcError,
11672
11481
  isNpxInstall,
11673
- isPlanStateEmpty,
11674
11482
  isPlausibleKey,
11675
11483
  listFilesSync,
11676
11484
  listFilesWithStatsAsync,
@@ -11707,6 +11515,7 @@ export {
11707
11515
  registerPlanTool,
11708
11516
  registerShellTools,
11709
11517
  registerSubagentTool,
11518
+ registerTodoTool,
11710
11519
  registerWebTools,
11711
11520
  renderMarkdown as renderDiffMarkdown,
11712
11521
  renderSummaryTable as renderDiffSummary,
@@ -11714,7 +11523,6 @@ export {
11714
11523
  replayFromFile,
11715
11524
  resolveExecutable,
11716
11525
  restoreSnapshots,
11717
- runBranches,
11718
11526
  runCommand,
11719
11527
  runHooks,
11720
11528
  sanitizeMemoryName,