mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -30,11 +30,14 @@ import {
30
30
  import {
31
31
  cacheGet,
32
32
  cacheSet,
33
+ runResultCacheInFlight,
33
34
  statPathsForMtime,
34
35
  } from './cache-layers.mjs';
35
36
  import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
36
37
  import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
37
38
 
39
+ const MIXDOG_GREP_CASE_HINT_PROBE = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_GREP_CASE_HINT_PROBE || ''));
40
+
38
41
  function expandLegacyEscapedAlternationPattern(rawPattern) {
39
42
  if (typeof rawPattern !== 'string' || !rawPattern.includes('\\|')) return null;
40
43
  const parts = rawPattern.split('\\|').map((part) => part.trim()).filter(Boolean);
@@ -180,6 +183,14 @@ function splitGlobString(value) {
180
183
  return out;
181
184
  }
182
185
 
186
+ function isRedundantAllFilesGlob(value) {
187
+ const g = canonicalizeGlobSlashes(String(value || '').trim())
188
+ .replace(/^\.\//, '')
189
+ .replace(/^\/+/, '')
190
+ .replace(/\/+$/, '');
191
+ return g === '**/*' || g === '**';
192
+ }
193
+
183
194
  function resolveSearchScope(root, workDir) {
184
195
  return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
185
196
  }
@@ -226,46 +237,6 @@ function parseGrepCountLine(line) {
226
237
  return { path, count };
227
238
  }
228
239
 
229
- // Per-pattern file-count probe for array grby: runs ONE rg --files-with-matches
230
- // per pattern (reusing the same scope/glob/type/flags as the merged search) so
231
- // the summary line can surface patterns that matched zero files — otherwise the
232
- // merged result hides which member of the array contributed nothing.
233
- async function _perPatternFileCounts({ patterns, searchPath, globPatterns, caseInsensitive, multilineMode, fileType, workDir }) {
234
- const results = await Promise.all(patterns.map(async (pattern) => {
235
- try {
236
- const rgArgs = buildGrepRgArgs({
237
- patterns: [pattern],
238
- searchPath,
239
- globPatterns,
240
- outputMode: 'files_with_matches',
241
- caseInsensitive,
242
- showLineNumbers: false,
243
- beforeN: null,
244
- afterN: null,
245
- contextN: null,
246
- multilineMode,
247
- fileType,
248
- onlyMatching: false,
249
- });
250
- const stdout = await runRg(rgArgs, { cwd: workDir });
251
- // A boxed partial/truncated stdout means the count is not the true
252
- // total — render '?' rather than a misleadingly-low or zero number.
253
- if (stdout && typeof stdout === 'object' && (stdout.partial || stdout.truncated)) return null;
254
- return String(stdout).split('\n').filter(Boolean).length;
255
- } catch {
256
- // Probe threw → count unknown, NOT zero.
257
- return null;
258
- }
259
- }));
260
- // Quote first (so newline/control chars can't inject footer lines), then
261
- // truncate the quoted string to 40 chars — matches the no-match path.
262
- const trunc = (p) => {
263
- const q = JSON.stringify(p);
264
- return q.length > 40 ? `${q.slice(0, 40)}...` : q;
265
- };
266
- return `\n# per-pattern: ${patterns.map((p, i) => `${trunc(p)}=${results[i] === null ? '?' : `${results[i]} files`}`).join(', ')}`;
267
- }
268
-
269
240
  function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns: _patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath: _grepResolvedPath, workDir, globPatterns: _globPatterns, fileType: _fileType, filenameOmitted = false, prefix = '', broadAdvisory: _broadAdvisory = true }) {
270
241
  const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
271
242
  const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
@@ -378,7 +349,9 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
378
349
  // ripgrep `--glob` uses forward slashes on all platforms; canonicalize
379
350
  // `\`→`/` (win32 only) so a `**\*.ts` filter matches instead of being
380
351
  // parsed as an escape sequence.
381
- const normalizedGlobPatterns = uniqueStrings(globPatterns.map(canonicalizeGlobSlashes));
352
+ const normalizedGlobPatterns = uniqueStrings(globPatterns
353
+ .map(canonicalizeGlobSlashes)
354
+ .filter((g) => !isRedundantAllFilesGlob(g)));
382
355
 
383
356
  const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'count']);
384
357
  const rawOutputMode = typeof args.output_mode === 'string' ? args.output_mode.trim() : '';
@@ -484,6 +457,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
484
457
  // drift detection. So only the fresh-compute path (below) records a read.
485
458
  if (cached !== null) return cached;
486
459
 
460
+ return await runResultCacheInFlight(cacheKey, async () => {
487
461
  let grepStat;
488
462
  try { grepStat = statSync(grepResolvedPath); }
489
463
  catch (err) {
@@ -597,7 +571,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
597
571
  // pre-offset matches) just means the window skipped past real
598
572
  // case-sensitive hits, so the hint would be misleading.
599
573
  const trueZeroMatch = offset === 0 && totalWindowed === 0;
600
- if (trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
574
+ if (MIXDOG_GREP_CASE_HINT_PROBE && trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
601
575
  try {
602
576
  const probeArgs = buildGrepRgArgs({
603
577
  patterns,
@@ -620,21 +594,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
620
594
  } catch { /* best-effort hint */ }
621
595
  }
622
596
  }
623
- // Array-pattern visibility: append a per-pattern file-count summary so
624
- // zero-hit members of the merged result are not silently hidden.
625
- let perPatternSummary = '';
626
- if (patterns.length > 1) {
627
- perPatternSummary = await _perPatternFileCounts({
628
- patterns,
629
- searchPath,
630
- globPatterns: normalizedGlobPatterns,
631
- caseInsensitive,
632
- multilineMode,
633
- fileType,
634
- workDir: rgSpawnCwd,
635
- });
636
- }
637
- const out = body + rgPartialSuffix + perPatternSummary;
597
+ const out = body + rgPartialSuffix;
638
598
  const shownLines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
639
599
  const remaining = Math.max(0, totalWindowed - shownLines.length);
640
600
  // Mirrors formatGrepOutput truncation / totalKnown semantics.
@@ -749,6 +709,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
749
709
  const msg = stderr || err?.message || String(err);
750
710
  return `Error: ${msg.slice(0, 500)}`;
751
711
  }
712
+ });
752
713
  }
753
714
 
754
715
  export async function executeGlobTool(args, workDir, options = {}) {
@@ -835,6 +796,8 @@ export async function executeGlobTool(args, workDir, options = {}) {
835
796
  return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
836
797
  }
837
798
  const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
799
+ const rawSort = typeof args.sort === 'string' ? args.sort.trim() : '';
800
+ const sortMode = rawSort === 'mtime' ? 'mtime' : 'natural';
838
801
  // Internal-only ignore extension (see normalizeGlobArgs). Caller (e.g.
839
802
  // ai-wrapped-dispatch broad-cwd preflight) appends basename ignore globs
840
803
  // so head_limit bounds SOURCE entries rather than artifact noise.
@@ -860,10 +823,11 @@ export async function executeGlobTool(args, workDir, options = {}) {
860
823
  .map((root) => normalizeOutputPath(resolvedForSearchRoot(root)))
861
824
  .sort()
862
825
  .join('\x01');
863
- const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs });
826
+ const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs, sort: sortMode });
864
827
  const cached = cacheGet(cacheKey);
865
828
  if (cached !== null) return cached;
866
829
 
830
+ return await runResultCacheInFlight(cacheKey, async () => {
867
831
  const globGroups = [...groups.entries()];
868
832
 
869
833
  const allFiles = [];
@@ -929,23 +893,26 @@ export async function executeGlobTool(args, workDir, options = {}) {
929
893
  }
930
894
 
931
895
  const unique = Array.from(new Set(allFiles));
932
- // Bound the post-rg stat phase: a single hung stat (dead mount /
933
- // unresponsive network path) must not pin glob until the 600s bridge stall
934
- // watchdog. Per-stat 5s deadline a hung entry is treated as stat-failed
935
- // and dropped, while normal local stats (sub-ms) are unaffected.
936
- const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
937
- const withStat = withStatAll.filter((entry) => entry?.stat != null);
938
- withStat.sort((a, b) => {
939
- const dm = b.mtime - a.mtime;
940
- if (dm !== 0) return dm;
941
- return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
942
- });
943
- const totalBeforeOffset = withStat.length;
944
- const windowed = offset > 0 ? withStat.slice(offset) : withStat;
945
- const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit)).map((entry) => {
946
- const abs = entry.full || resolveAgainstCwd(entry.path, workDir);
947
- return relativeSearchResultPath(abs, workDir);
948
- });
896
+ let orderedPaths;
897
+ if (sortMode === 'mtime') {
898
+ // Opt-in mtime sorting is intentionally slower: it stats every match.
899
+ // Bound the post-rg stat phase so a hung mount cannot pin glob until
900
+ // the agent stall watchdog fires.
901
+ const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
902
+ const withStat = withStatAll.filter((entry) => entry?.stat != null);
903
+ withStat.sort((a, b) => {
904
+ const dm = b.mtime - a.mtime;
905
+ if (dm !== 0) return dm;
906
+ return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
907
+ });
908
+ orderedPaths = withStat.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
909
+ } else {
910
+ orderedPaths = unique.map((entry) => isAbsolute(entry) ? resolve(entry) : resolveAgainstCwd(entry, workDir));
911
+ }
912
+ const totalBeforeOffset = orderedPaths.length;
913
+ const windowed = offset > 0 ? orderedPaths.slice(offset) : orderedPaths;
914
+ const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit))
915
+ .map((abs) => relativeSearchResultPath(abs, workDir));
949
916
  const remaining = windowed.length - capped.length;
950
917
  const truncSuffix = accumTruncated
951
918
  ? '\n... [truncated at accumulation cap (50000)]'
@@ -979,4 +946,5 @@ export async function executeGlobTool(args, workDir, options = {}) {
979
946
  try { options.onProgress(`found ${totalBeforeOffset} files`); } catch { /* best-effort */ }
980
947
  }
981
948
  return out;
949
+ });
982
950
  }
@@ -1,4 +1,4 @@
1
- import { spawn } from 'child_process';
1
+ import { spawn, spawnSync } from 'child_process';
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, watch as fsWatch, writeFileSync } from 'fs';
3
3
  import * as fsPromises from 'fs/promises';
4
4
  import { basename, join } from 'path';
@@ -15,6 +15,8 @@ import {
15
15
  } from '../../../../shared/background-tasks.mjs';
16
16
  import { startChildGuardian } from '../../../../shared/child-guardian.mjs';
17
17
 
18
+ globalThis.__mixdogShellJobsRuntimeLoaded = true;
19
+
18
20
  function sleep(ms) {
19
21
  return new Promise((resolve) => setTimeout(resolve, ms));
20
22
  }
@@ -220,29 +222,62 @@ function killProcessTree(pid, signal = 'SIGTERM') {
220
222
  // owned children. Async jobs are intentionally CLI-owned; no restart replay or
221
223
  // daemon handoff is attempted.
222
224
  const _liveJobPids = new Set();
225
+ const _liveJobIdsByPid = new Map();
223
226
  let _shellJobsExitHookInstalled = false;
224
- function _registerLiveJobPid(pid) {
225
- if (Number.isFinite(pid) && pid > 0) _liveJobPids.add(pid);
227
+ function _registerLiveJobPid(pid, jobId = null) {
228
+ if (Number.isFinite(pid) && pid > 0) {
229
+ _liveJobPids.add(pid);
230
+ if (jobId) _liveJobIdsByPid.set(pid, jobId);
231
+ }
226
232
  }
227
233
  function _unregisterLiveJobPid(pid) {
228
- if (Number.isFinite(pid) && pid > 0) _liveJobPids.delete(pid);
234
+ if (Number.isFinite(pid) && pid > 0) {
235
+ _liveJobPids.delete(pid);
236
+ _liveJobIdsByPid.delete(pid);
237
+ }
229
238
  }
230
- function _sweepLiveJobsSync() {
231
- for (const pid of _liveJobPids) {
232
- try {
233
- if (process.platform === 'win32') {
234
- spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
239
+ function _killLiveJobPid(pid, { sync = false } = {}) {
240
+ try {
241
+ if (process.platform === 'win32') {
242
+ if (sync) {
243
+ spawnSync('taskkill.exe', ['/pid', String(pid), '/t', '/f'], {
244
+ windowsHide: true,
245
+ stdio: 'ignore',
246
+ timeout: 1500,
247
+ });
235
248
  } else {
236
- try { process.kill(-pid, 'SIGKILL'); }
237
- catch { try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ } }
249
+ spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
238
250
  }
239
- } catch { /* ignore */ }
251
+ } else {
252
+ try { process.kill(-pid, 'SIGKILL'); }
253
+ catch { try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ } }
254
+ }
255
+ return true;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+ function _sweepLiveJobsSync() {
261
+ for (const pid of _liveJobPids) {
262
+ _killLiveJobPid(pid, { sync: true });
240
263
  }
241
264
  _liveJobPids.clear();
265
+ _liveJobIdsByPid.clear();
266
+ }
267
+ function _ensureProcessListenerHeadroom(events, extra = 1) {
268
+ try {
269
+ if (typeof process.getMaxListeners !== 'function' || typeof process.setMaxListeners !== 'function') return;
270
+ const current = process.getMaxListeners();
271
+ if (current === 0) return;
272
+ let needed = current;
273
+ for (const event of events) needed = Math.max(needed, process.listenerCount(event) + extra);
274
+ if (needed > current) process.setMaxListeners(needed);
275
+ } catch { /* ignore */ }
242
276
  }
243
277
  function _installShellJobsExitHook() {
244
278
  if (_shellJobsExitHookInstalled) return;
245
279
  _shellJobsExitHookInstalled = true;
280
+ _ensureProcessListenerHeadroom(['exit', 'SIGTERM', 'SIGINT', 'SIGHUP'], 1);
246
281
  try { process.on('exit', _sweepLiveJobsSync); } catch { /* ignore */ }
247
282
  try { process.on('SIGTERM', _sweepLiveJobsSync); } catch { /* ignore */ }
248
283
  try { process.on('SIGINT', _sweepLiveJobsSync); } catch { /* ignore */ }
@@ -538,8 +573,8 @@ function refreshShellJob(jobId) {
538
573
  return detail;
539
574
  }
540
575
 
541
- export function startBackgroundShellJob({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellType, clientHostPid }) {
542
- return _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellType, clientHostPid });
576
+ export function startBackgroundShellJob({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellArgs, shellType, clientHostPid }) {
577
+ return _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellArgs, shellType, clientHostPid });
543
578
  }
544
579
 
545
580
  // In-process completion watcher. After a background shell task is spawned the
@@ -572,6 +607,20 @@ const jobNotifyCtxByJobId = new Map();
572
607
  // consuming the outcome, so the watcher must stay cancelled; the last waiter to
573
608
  // leave (count===0) owns the decision to re-arm a still-running job.
574
609
  const jobWaitWaiterCountByJobId = new Map();
610
+ function markShellJobCancelledByShutdown(jobId, reason = 'shutdown') {
611
+ const detail = readShellJobDetail(jobId);
612
+ if (!detail || detail.status !== 'running') return false;
613
+ detail.status = 'cancelled';
614
+ detail.exitCode = 137;
615
+ detail.killed = true;
616
+ detail.error = `cancelled by runtime shutdown (${reason})`;
617
+ detail.finishedAt = new Date().toISOString();
618
+ try { writeFileSync(detail.exitPath || shellJobExitPath(jobId), '137'); } catch { /* ignore */ }
619
+ try { writeFileSync(detail.donePath || shellJobDonePath(jobId), ''); } catch { /* ignore */ }
620
+ writeShellJobDetail(detail);
621
+ _unregisterLiveJobPid(detail.pid);
622
+ return true;
623
+ }
575
624
  // Register a synchronous task waiter. Paired with endShellJobWait in a
576
625
  // finally so the count can't leak on throw.
577
626
  export function beginShellJobWait(jobId) {
@@ -590,6 +639,26 @@ export function endShellJobWait(jobId) {
590
639
  export function clearShellJobNotifyCtx(jobId) {
591
640
  jobNotifyCtxByJobId.delete(jobId);
592
641
  }
642
+ export function shutdownShellJobs(reason = 'runtime-close', { sync = false } = {}) {
643
+ const livePids = [..._liveJobPids];
644
+ const jobIds = new Set([..._liveJobIdsByPid.values()].filter(Boolean));
645
+ const watcherJobIds = [...backgroundShellJobWatchers.keys()];
646
+ for (const jobId of watcherJobIds) {
647
+ jobIds.add(jobId);
648
+ try { cancelBackgroundShellJobWatch(jobId); } catch { /* ignore */ }
649
+ }
650
+ for (const pid of livePids) _killLiveJobPid(pid, { sync });
651
+ _liveJobPids.clear();
652
+ _liveJobIdsByPid.clear();
653
+ let marked = 0;
654
+ for (const jobId of jobIds) {
655
+ try { if (markShellJobCancelledByShutdown(jobId, reason)) marked += 1; } catch { /* ignore */ }
656
+ }
657
+ backgroundShellJobWatchers.clear();
658
+ jobNotifyCtxByJobId.clear();
659
+ jobWaitWaiterCountByJobId.clear();
660
+ return { killed: livePids.length, cancelledJobs: marked, cancelledWatchers: watcherJobIds.length };
661
+ }
593
662
  // Cancel (and unregister) an armed watcher without notifying. Idempotent: a
594
663
  // no-op when no watcher is armed, and the per-watcher cancel respects the
595
664
  // `settled` guard so it cannot race a real completion notify. The persistent
@@ -824,12 +893,12 @@ export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStd
824
893
  writeShellJobDetail(detail);
825
894
  if (Number.isFinite(pid) && pid > 0) {
826
895
  _installShellJobsExitHook();
827
- _registerLiveJobPid(pid);
896
+ _registerLiveJobPid(pid, jobId);
828
897
  }
829
898
  return { ...detail, exitPath, donePath };
830
899
  }
831
900
 
832
- function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellType, clientHostPid }) {
901
+ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellArgs, shellType, clientHostPid }) {
833
902
  // Route ANY PowerShell shell to the PS wrapper, regardless of platform.
834
903
  // Gating on win32 sent shell:'powershell' on macOS/Linux down the POSIX
835
904
  // path below, which spawns `pwsh <wrapper>.sh` — pwsh then tries to run a
@@ -871,7 +940,8 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
871
940
  // it trivial to distinguish a timeout (124) from a user-side
872
941
  // SIGTERM exit (143).
873
942
  const innerShellQ = shellQuoteSingle(shell);
874
- const innerArgQ = shellQuoteSingle(shellArg);
943
+ const innerArgs = Array.isArray(shellArgs) && shellArgs.length > 0 ? shellArgs : [shellArg];
944
+ const innerArgsQ = innerArgs.filter((arg) => arg != null && String(arg).length > 0).map(shellQuoteSingle).join(' ');
875
945
  // Runtime enforcement proof: the wrapper touches <jobId>.enforced right
876
946
  // before exec'ing `timeout`, so the marker exists iff the timeout branch
877
947
  // actually ran under the wrapper's own env/cwd. A spawn-time probe can't
@@ -885,7 +955,7 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
885
955
  // status for processes that actually exited 0. `rm -- "$0"` removes
886
956
  // the staged wrapper .cmd.sh after donePath is published so a host
887
957
  // crash before this point still leaves the file for the sweep to GC.
888
- const wrapped = `{ if command -v timeout >/dev/null 2>&1; then _to=timeout; elif command -v gtimeout >/dev/null 2>&1; then _to=gtimeout; else _to=; fi; if [ -n "$_to" ]; then touch ${shellQuoteSingle(enforcedPath)}; "$_to" ${timeoutSeconds} ${innerShellQ} ${innerArgQ} ${userCmdQuoted}; else ${innerShellQ} ${innerArgQ} ${userCmdQuoted}; fi; rc=$?; printf '%s' "$rc" > ${shellQuoteSingle(exitPath)}; touch ${shellQuoteSingle(donePath)}; rm -- "$0" 2>/dev/null; exit $rc; }`;
958
+ const wrapped = `{ if command -v timeout >/dev/null 2>&1; then _to=timeout; elif command -v gtimeout >/dev/null 2>&1; then _to=gtimeout; else _to=; fi; if [ -n "$_to" ]; then touch ${shellQuoteSingle(enforcedPath)}; "$_to" ${timeoutSeconds} ${innerShellQ} ${innerArgsQ} ${userCmdQuoted}; else ${innerShellQ} ${innerArgsQ} ${userCmdQuoted}; fi; rc=$?; printf '%s' "$rc" > ${shellQuoteSingle(exitPath)}; touch ${shellQuoteSingle(donePath)}; rm -- "$0" 2>/dev/null; exit $rc; }`;
889
959
  // Stage the wrapped command to a .sh and let the script open its own
890
960
  // output files via `exec > … 2> …`. The parent does NOT pass file
891
961
  // descriptors via stdio inheritance (`stdio: 'ignore'` for all three).
@@ -919,7 +989,7 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
919
989
  label: 'shell-job',
920
990
  });
921
991
  _installShellJobsExitHook();
922
- _registerLiveJobPid(child.pid);
992
+ _registerLiveJobPid(child.pid, jobId);
923
993
  const detail = {
924
994
  jobId,
925
995
  kind: 'bash',
@@ -1056,7 +1126,7 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
1056
1126
  return { jobId, kind: 'bash', status: 'failed', error: 'PowerShell background task spawn returned no pid' };
1057
1127
  }
1058
1128
  _installShellJobsExitHook();
1059
- _registerLiveJobPid(childPid);
1129
+ _registerLiveJobPid(childPid, jobId);
1060
1130
  const detail = {
1061
1131
  jobId,
1062
1132
  kind: 'bash',
@@ -19,6 +19,7 @@ import {
19
19
  } from './builtin/bash-tool.mjs';
20
20
  import {
21
21
  executeFindFilesTool,
22
+ executeFuzzyFindTool,
22
23
  executeListTool,
23
24
  executeTreeTool,
24
25
  } from './builtin/list-tool.mjs';
@@ -129,7 +130,7 @@ export {
129
130
  } from './builtin/cache-layers.mjs';
130
131
  export { atomicWrite } from './builtin/atomic-write.mjs';
131
132
  // ---------------------------------------------------------------------------
132
- // User-cwd persistence bridge: hook writes user-cwd.txt on SessionStart so
133
+ // User-cwd persistence hook: writes user-cwd.txt on SessionStart so
133
134
  // the MCP server (spawned from cache dir) resolves the correct sandbox root.
134
135
  // Helper extracted to src/shared/user-cwd.mjs so server-main.mjs can import
135
136
  // the same primitive without circular-import risk.
@@ -252,9 +253,54 @@ const BUILTIN_TOOL_ALIASES = new Map([
252
253
  ['golb', 'glob'],
253
254
  ]);
254
255
 
256
+ // Case-insensitive lookup of canonical builtin names, built once from the
257
+ // declarative BUILTIN_TOOLS registry. Models trained on other coding-agent
258
+ // CLIs frequently emit PascalCase tool names ("Read", "Shell", "Grep",
259
+ // "List") for tools mixdog exposes in lowercase. Those calls used to fall
260
+ // through to formatUnknownBuiltinToolMessage and, because the error did not
261
+ // resolve the case mismatch, the model re-issued the SAME capitalised call
262
+ // every iteration until the loop cap. A lowercase→canonical map makes the
263
+ // dispatcher accept the case variant directly.
264
+ const _BUILTIN_LOWER_TO_CANONICAL = new Map(
265
+ BUILTIN_TOOLS.map((t) => [String(t.name || '').toLowerCase(), t.name]).filter(([k]) => k),
266
+ );
267
+
268
+ // Well-known tool names from OTHER agent CLIs that have no mixdog builtin.
269
+ // They cannot be silently remapped (their argument shapes differ — e.g.
270
+ // Write{path,contents} vs apply_patch{patch}) so the unknown-tool error
271
+ // surfaces a concrete redirect instead of a bare "unknown tool", which is
272
+ // what kept models looping on a non-existent tool.
273
+ const EXTERNAL_TOOL_REDIRECTS = new Map([
274
+ ['write', 'use `apply_patch` with an `*** Add File:` V4A section to create a file'],
275
+ ['edit', 'use `apply_patch` with a `*** Update File:` V4A section'],
276
+ ['multiedit', 'use a single `apply_patch` call with multiple `*** Update File:` sections'],
277
+ ['strreplace', 'use `apply_patch` with a `*** Update File:` V4A section'],
278
+ ['str_replace', 'use `apply_patch` with a `*** Update File:` V4A section'],
279
+ ['str_replace_editor', 'use `apply_patch` with a `*** Update File:` V4A section'],
280
+ ['createfile', 'use `apply_patch` with an `*** Add File:` V4A section'],
281
+ ['create_file', 'use `apply_patch` with an `*** Add File:` V4A section'],
282
+ ['deletefile', 'use `apply_patch` with a `*** Delete File:` V4A section'],
283
+ ['delete_file', 'use `apply_patch` with a `*** Delete File:` V4A section'],
284
+ ['bash', 'use the `shell` tool'],
285
+ ['run', 'use the `shell` tool'],
286
+ ['runcommand', 'use the `shell` tool'],
287
+ ['terminal', 'use the `shell` tool'],
288
+ ['view', 'use the `read` tool'],
289
+ ['cat', 'use the `read` tool'],
290
+ ['ls', 'use the `list` tool'],
291
+ ['searchfiles', 'use the `grep` tool'],
292
+ ['codebase_search', 'use the `grep` or `code_graph` tool'],
293
+ ]);
294
+
255
295
  export function canonicalizeBuiltinToolName(name) {
256
296
  if (typeof name !== 'string') return name;
257
- return BUILTIN_TOOL_ALIASES.get(name) || name;
297
+ const aliased = BUILTIN_TOOL_ALIASES.get(name);
298
+ if (aliased) return aliased;
299
+ // Exact match wins (cheapest, most common path).
300
+ if (_BUILTIN_LOWER_TO_CANONICAL.get(name) === name) return name;
301
+ // Case-insensitive fall-through: "Read"/"SHELL"/"Grep" → canonical.
302
+ const lowerCanonical = _BUILTIN_LOWER_TO_CANONICAL.get(name.toLowerCase());
303
+ return lowerCanonical || name;
258
304
  }
259
305
 
260
306
  function editDistance(a, b) {
@@ -294,6 +340,12 @@ export function suggestBuiltinToolName(name) {
294
340
  }
295
341
 
296
342
  export function formatUnknownBuiltinToolMessage(name, _args = {}, noun = 'builtin tool') {
343
+ // Concrete redirect for a well-known tool from another agent CLI whose
344
+ // argument shape differs from any mixdog builtin (so it cannot be
345
+ // case-normalised into one). Without this, the model re-issues the same
346
+ // non-existent tool every iteration until the loop cap.
347
+ const redirect = typeof name === 'string' ? EXTERNAL_TOOL_REDIRECTS.get(name.toLowerCase()) : null;
348
+ if (redirect) return `Error: unknown ${noun} "${name}" — ${redirect}.`;
297
349
  const suggestion = suggestBuiltinToolName(name);
298
350
  if (!suggestion) return `Error: unknown ${noun} "${name}"`;
299
351
  return `Error: unknown ${noun} "${name}". Did you mean "${suggestion}"?`;
@@ -308,7 +360,7 @@ process.on('exit', flushReadRangeIndexesSync);
308
360
 
309
361
  configureReadRangeIndexTelemetry({ trace: _ioTrace, hashText: _hashText });
310
362
 
311
- // Uniform tool-output cap (Codex `tool_output_token_limit` analogue): a SINGLE
363
+ // Uniform tool-output cap (`tool_output_token_limit` analogue): a SINGLE
312
364
  // optional knob that bounds EVERY builtin string result before it enters the
313
365
  // lead context, on top of each tool's own caps. Default 0 = OFF (per-tool caps
314
366
  // only, no behaviour change). Set MIXDOG_TOOL_OUTPUT_MAX_BYTES (or pass
@@ -342,7 +394,7 @@ function capToolOutput(result, options = {}) {
342
394
  const bytes = Buffer.byteLength(result, 'utf8');
343
395
  if (bytes <= cap) return result;
344
396
  // Middle-truncate: keep head + tail (byte-accurate), drop the middle. Mirrors
345
- // Codex middle truncation so the model still sees both ends of a runaway.
397
+ // Middle truncation so the model still sees both ends of a runaway.
346
398
  const half = Math.max(1, Math.floor(cap / 2) - 64);
347
399
  const head = _sliceToBytesFromStart(result, half);
348
400
  const tail = _sliceToBytesFromEnd(result, half);
@@ -371,7 +423,7 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
371
423
  const readStateScope = options?.readStateScope ?? options?.sessionId ?? null;
372
424
  const executeChildBuiltinTool = (childName, childArgs, childCwd = workDir, childOptions = null) =>
373
425
  executeBuiltinTool(childName, childArgs, childCwd, childOptions ? { ...options, ...childOptions } : options);
374
- // Path policy: Claude Code's settings.json permissions (mcp__* allow) are the
426
+ // Path policy: host settings.json permissions (mcp__* allow) are the
375
427
  // sole arbiter for workspace-boundary decisions.
376
428
  const _toolResult = await (async () => {
377
429
  switch (toolName) {
@@ -389,6 +441,8 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
389
441
  return executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope, options);
390
442
  case 'glob':
391
443
  return executeGlobTool(args, workDir, options);
444
+ case 'find':
445
+ return executeFuzzyFindTool(args, workDir, options);
392
446
  case 'list':
393
447
  return executeListTool(args, workDir, options);
394
448
  case 'tree':
@@ -0,0 +1,86 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, resolve as pathResolve } from 'node:path';
3
+
4
+ export const codeGraphCache = new Map();
5
+ export const codeGraphDirtyPaths = new Map();
6
+
7
+ const codeGraphDirtyGen = new Map();
8
+ let drainCodeGraphCacheFn = null;
9
+
10
+ export function canonicalGraphCwd(cwd) {
11
+ if (!cwd) throw new Error('code_graph requires cwd - caller did not provide a working directory');
12
+ const full = pathResolve(cwd);
13
+ return process.platform === 'win32' ? full.toLowerCase() : full;
14
+ }
15
+
16
+ export function canonicalGraphPath(p) {
17
+ const full = pathResolve(String(p || ''));
18
+ return process.platform === 'win32' ? full.toLowerCase() : full;
19
+ }
20
+
21
+ export function getCodeGraphGen(graphCwd) {
22
+ return codeGraphDirtyGen.get(graphCwd) || 0;
23
+ }
24
+
25
+ export function bumpCodeGraphGen(graphCwd) {
26
+ codeGraphDirtyGen.set(graphCwd, (codeGraphDirtyGen.get(graphCwd) || 0) + 1);
27
+ }
28
+
29
+ export function consumeCodeGraphDirtyPaths(cwd) {
30
+ const key = canonicalGraphCwd(cwd);
31
+ const set = codeGraphDirtyPaths.get(key);
32
+ if (!set || set.size === 0) return [];
33
+ codeGraphDirtyPaths.delete(key);
34
+ return [...set];
35
+ }
36
+
37
+ // Accept absolute written paths only; resolve affected indexed roots centrally.
38
+ export function markCodeGraphDirtyPaths(paths) {
39
+ const values = Array.isArray(paths) ? paths : [paths];
40
+ const cleaned = values
41
+ .filter(Boolean)
42
+ .map((p) => canonicalGraphPath(p));
43
+ if (cleaned.length === 0) return;
44
+
45
+ const knownRoots = new Set([...codeGraphDirtyPaths.keys(), ...codeGraphCache.keys()]);
46
+ const affectedRoots = new Set();
47
+ for (const absPath of cleaned) {
48
+ let matchedThisPath = false;
49
+ for (const root of knownRoots) {
50
+ const canonRoot = canonicalGraphPath(root);
51
+ if (absPath.startsWith(canonRoot + '/') || absPath.startsWith(canonRoot + '\\') || absPath === canonRoot) {
52
+ affectedRoots.add(root);
53
+ matchedThisPath = true;
54
+ }
55
+ }
56
+ if (!matchedThisPath) {
57
+ let dir = dirname(absPath);
58
+ while (dir && dir !== dirname(dir)) {
59
+ if (existsSync(join(dir, 'package.json')) || existsSync(join(dir, '.git'))) {
60
+ affectedRoots.add(canonicalGraphCwd(dir));
61
+ break;
62
+ }
63
+ dir = dirname(dir);
64
+ }
65
+ }
66
+ }
67
+
68
+ for (const root of affectedRoots) {
69
+ if (!codeGraphDirtyPaths.has(root)) codeGraphDirtyPaths.set(root, new Set());
70
+ const set = codeGraphDirtyPaths.get(root);
71
+ for (const p of cleaned) set.add(p);
72
+
73
+ const canonRoot = canonicalGraphCwd(root);
74
+ codeGraphCache.delete(canonRoot);
75
+ bumpCodeGraphGen(canonRoot);
76
+ }
77
+ }
78
+
79
+ export function registerCodeGraphDrain(fn) {
80
+ drainCodeGraphCacheFn = typeof fn === 'function' ? fn : null;
81
+ }
82
+
83
+ export function drainCodeGraphCache() {
84
+ if (!drainCodeGraphCacheFn) return;
85
+ drainCodeGraphCacheFn();
86
+ }
@@ -3,20 +3,20 @@ export const CODE_GRAPH_TOOL_DEFS = [
3
3
  name: 'code_graph',
4
4
  title: 'Code Graph',
5
5
  annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
6
- description: 'Inspect code structure. Top-level entry for code-related questions: symbols, definitions, references, callers, callees, imports, and related files. Use before read when structure can narrow the target; use grep for exact text.',
6
+ description: 'Code structure: symbols, refs, calls, imports, impact.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
10
- mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'search', 'references', 'callers', 'callees', 'prewarm'], description: 'Operation: repo/file overview, import graph, symbol lookup/search, references, callers/callees, or cache prewarm.' },
11
- file: { type: 'string', description: 'Target source file. Directory scope is only for references/callers; omit for repo-wide overview/search.' },
12
- symbol: { type: 'string', description: 'Identifier or search keyword for symbol/search/reference/call modes.' },
13
- symbols: { type: 'array', items: { type: 'string' }, description: 'Batch identifiers for find_symbol/callers/callees/references/prewarm.' },
14
- body: { type: 'boolean', description: 'Return full symbol body for find_symbol. Default true.' },
15
- language: { type: 'string', description: 'Optional language hint.' },
16
- limit: { type: 'number', minimum: 1, description: 'Max results to return; clamped per mode.' },
17
- depth: { type: 'number', minimum: 1, maximum: 5, description: 'callers only: 1 direct, 2-5 transitive.' },
18
- page: { type: 'number', minimum: 1, description: 'callers depth>=2 page.' },
19
- cwd: { type: 'string', description: 'Project root override; omit when runtime cwd is already the repo root.' },
10
+ mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'search', 'references', 'callers', 'callees', 'prewarm'], description: 'Operation.' },
11
+ file: { type: 'string', description: 'Source file.' },
12
+ symbol: { type: 'string', description: 'Identifier or symbol keyword.' },
13
+ symbols: { type: 'array', items: { type: 'string' }, description: 'Batch identifiers.' },
14
+ body: { type: 'boolean', description: 'Include body.' },
15
+ language: { type: 'string', description: 'Language hint.' },
16
+ limit: { type: 'number', minimum: 1, description: 'Max results.' },
17
+ depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
18
+ page: { type: 'number', minimum: 1, description: 'Caller page.' },
19
+ cwd: { type: 'string', description: 'Project root.' },
20
20
  },
21
21
  required: ['mode'],
22
22
  },