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
@@ -41,14 +41,16 @@ import * as nodeUtil from 'node:util';
41
41
  import { existsSync } from 'node:fs';
42
42
  import { randomUUID } from 'node:crypto';
43
43
  import { invalidateBuiltinResultCache, analyzeShellCommandEffects, preflightShellLargeFileProbe } from './builtin.mjs';
44
- import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph.mjs';
44
+ import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph-state.mjs';
45
45
  import { maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
46
46
  import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
47
- import { _captureTrackedMtimes, _trackedDriftNoteAfter, _injectionBlockTargets, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
47
+ import { _captureTrackedMtimes, _trackedDriftNoteAfter, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
48
48
  import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
49
49
  import { checkExecPolicyMessage } from './bash-policy-scan.mjs';
50
50
  import { startChildGuardian } from '../../../shared/child-guardian.mjs';
51
51
 
52
+ globalThis.__mixdogBashSessionRuntimeLoaded = true;
53
+
52
54
  // Default 600 s (10 min), max 1800 s. Aligned with the one-shot bash tool's
53
55
  // 600 s default (builtin/bash-tool.mjs); the persistent shell carries
54
56
  // longer-running pipelines (build/test/install) than one-shot calls, so a
@@ -171,15 +173,18 @@ function _startReaper() {
171
173
  _killSession(id, 'idle-timeout');
172
174
  }
173
175
  }
174
- if (_sessions.size === 0) {
175
- clearInterval(_reaperTimer);
176
- _reaperTimer = null;
177
- }
176
+ _clearReaperIfIdle();
178
177
  }, 30_000);
179
178
  // Don't keep the event loop alive just for the reaper.
180
179
  if (typeof _reaperTimer.unref === 'function') _reaperTimer.unref();
181
180
  }
182
181
 
182
+ function _clearReaperIfIdle() {
183
+ if (_sessions.size !== 0 || !_reaperTimer) return;
184
+ clearInterval(_reaperTimer);
185
+ _reaperTimer = null;
186
+ }
187
+
183
188
  // Kill a spawned shell along with any child processes it forked. Posix path
184
189
  // signals the process group (pgid == pid because we spawn with detached:true),
185
190
  // so `sleep 1000 &` or a node server started inside the session is reaped
@@ -239,6 +244,17 @@ async function _killSession(id, _reason) {
239
244
  try { s.proc.stdin?.end(); } catch { /* ignore */ }
240
245
  _killProcessTree(s.proc);
241
246
  await Promise.race([exited, new Promise((r) => setTimeout(r, 3000))]);
247
+ _clearReaperIfIdle();
248
+ }
249
+
250
+ function _killSessionNow(id, _reason) {
251
+ const s = _sessions.get(id);
252
+ if (!s) return false;
253
+ _sessions.delete(id);
254
+ try { s.proc.stdin?.end(); } catch { /* ignore */ }
255
+ _killProcessTree(s.proc);
256
+ _clearReaperIfIdle();
257
+ return true;
242
258
  }
243
259
 
244
260
  function shellQuoteSingle(s) {
@@ -713,10 +729,21 @@ export function closeBashSession(sessionId, reason = 'external-close') {
713
729
  return true;
714
730
  }
715
731
 
732
+ export async function shutdownBashSessions(reason = 'runtime-close') {
733
+ const ids = [..._sessions.keys()];
734
+ await Promise.allSettled(ids.map((id) => _killSession(id, reason)));
735
+ _clearReaperIfIdle();
736
+ return { closed: ids.length };
737
+ }
738
+
716
739
  // Best-effort cleanup on process exit so orphan bash children don't linger
717
740
  // when the plugin host shuts down. Self-registered exit drain; bare 'exit' hook stays as idempotent backup.
718
- export function drainBashSessions() {
719
- for (const id of [..._sessions.keys()]) _killSession(id, 'process-exit');
741
+ export function drainBashSessions(reason = 'process-exit') {
742
+ let closed = 0;
743
+ for (const id of [..._sessions.keys()]) {
744
+ if (_killSessionNow(id, reason)) closed += 1;
745
+ }
746
+ return { closed };
720
747
  }
721
748
  if (typeof process?.on === 'function') {
722
749
  process.on('exit', drainBashSessions);
@@ -76,11 +76,6 @@ function isStringOrStringArray(v) {
76
76
  return true;
77
77
  }
78
78
 
79
- function hasBackslashPipe(value) {
80
- if (typeof value === 'string') return false;
81
- return false;
82
- }
83
-
84
79
  function hasUnsupportedRipgrepRegex(value) {
85
80
  const values = Array.isArray(value) ? value : [value];
86
81
  return values.some((item) => {
@@ -144,9 +139,6 @@ function guardGrep(a) {
144
139
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
145
140
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
146
141
  }
147
- if (hasOwn(a, k) && hasBackslashPipe(a[k])) {
148
- return `Error: grep arg "${k}" contains \\|. Use pattern:["a","b"] for OR terms, or unescaped | for regex alternation.`;
149
- }
150
142
  if (hasOwn(a, k) && hasUnsupportedRipgrepRegex(a[k])) {
151
143
  return `Error: grep arg "${k}" uses regex syntax ripgrep does not support here (lookaround/backrefs). Use plain pattern arrays or simpler regex.`;
152
144
  }
@@ -311,6 +303,24 @@ function guardList(a) {
311
303
  return null;
312
304
  }
313
305
 
306
+ function guardFind(a) {
307
+ if (!hasOwn(a, 'query') || typeof a.query !== 'string' || a.query.trim().length === 0) {
308
+ return `Error: find requires non-empty string "query" (got ${describeType(a.query)})`;
309
+ }
310
+ if (hasOwn(a, 'path') && !isString(a.path)) {
311
+ return `Error: find arg "path" must be a string (got ${describeType(a.path)})`;
312
+ }
313
+ if (hasOwn(a, 'head_limit') && a.head_limit !== undefined && a.head_limit !== null) {
314
+ if (!isFiniteInt(a.head_limit)) {
315
+ return `Error: find arg "head_limit" must be a finite integer (got ${describeType(a.head_limit)})`;
316
+ }
317
+ if (a.head_limit < 0) {
318
+ return `Error: find arg "head_limit" must be >= 0 (0 means no cap); got ${a.head_limit}`;
319
+ }
320
+ }
321
+ return null;
322
+ }
323
+
314
324
  function guardGlob(a) {
315
325
  // path alias root; pattern aliases glob/name/file_pattern
316
326
  for (const k of ['path', 'root']) {
@@ -365,6 +375,7 @@ const GUARDS = {
365
375
  shell: guardShell,
366
376
  task: guardTask,
367
377
  list: guardList,
378
+ find: guardFind,
368
379
  glob: guardGlob,
369
380
  code_graph: guardCodeGraph,
370
381
  };
@@ -3,8 +3,8 @@ import { execShellCommand, stripAnsi } from '../shell-command.mjs';
3
3
  import { wrapCommandWithSnapshot } from '../shell-snapshot.mjs';
4
4
  import { getDestructiveCommandWarning } from '../destructive-warning.mjs';
5
5
  import { maybeRewriteWmicProcessCommand } from '../shell-policy.mjs';
6
- import { buildBashPolicyScanTargets, checkExecPolicyMessage, injectionBlockTargets } from '../bash-policy-scan.mjs';
7
- import { markCodeGraphDirtyPaths, drainCodeGraphCache } from '../code-graph.mjs';
6
+ import { buildBashPolicyScanTargets, checkExecPolicyMessage } from '../bash-policy-scan.mjs';
7
+ import { markCodeGraphDirtyPaths, drainCodeGraphCache } from '../code-graph-state.mjs';
8
8
  import {
9
9
  buildJobNotFoundMessage,
10
10
  startBackgroundShellJob,
@@ -27,7 +27,6 @@ import {
27
27
  cancelBackgroundTask,
28
28
  completeBackgroundTask,
29
29
  getBackgroundTask,
30
- listBackgroundTasks,
31
30
  registerBackgroundTask,
32
31
  renderBackgroundTask,
33
32
  renderBackgroundTaskList,
@@ -48,7 +47,6 @@ import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
48
47
  // closing the "external write -> stale old_string -> code 8" gap when shell is
49
48
  // routed through this tool. Bounded to the tracked-read set (capped) so cost
50
49
  // stays off the whole-cwd path; emits nothing when no read file changed.
51
- const _DRIFT_SCAN_CAP = 256;
52
50
  export function _captureTrackedMtimes(_scope) {
53
51
  return new Map();
54
52
  }
@@ -60,7 +58,7 @@ export function _trackedDriftNoteAfter(_scope, _pre) {
60
58
  // AbortSignal (e.g. the MCP/request signal threaded through options.abortSignal).
61
59
  // Returns null when neither is present so existing session-only behavior is
62
60
  // preserved unchanged. Uses AbortSignal.any when available; falls back to a
63
- // manual controller + listener bridge otherwise. The returned signal aborts as
61
+ // manual controller + listener path otherwise. The returned signal aborts as
64
62
  // soon as either input signal aborts, which propagates to execShellCommand /
65
63
  // executeBashSessionTool and triggers the same child-kill path the session
66
64
  // signal already drives.
@@ -86,53 +84,12 @@ function _combineAbortSignals(sessionSignal, externalSignal) {
86
84
  return ctl.signal;
87
85
  }
88
86
 
89
- // Decode ANSI-C $'…' and locale $"…" escapes so the blocklist scan sees the
90
- // literal command (e.g. $'\x72m' → "rm"). Defensive against quoting bypass.
91
- function _decodeAnsiCQuotes(s) {
92
- if (typeof s !== 'string') return '';
93
- if (s.indexOf('$') === -1) return s;
94
- return s.replace(/\$(['"])((?:\\.|[^\\])*?)\1/g, (_full, _q, body) =>
95
- body
96
- .replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
97
- .replace(/\\u([0-9a-fA-F]{1,4})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
98
- .replace(/\\0([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
99
- .replace(/\\([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
100
- .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
101
- .replace(/\\\\/g, '\\').replace(/\\(['"])/g, '$1'),
102
- );
103
- }
104
-
105
- // Extract $(…) and `…` command-substitution bodies so each is re-scanned by
106
- // isBlockedCommand (e.g. eval $(printf 'rm -rf ~')).
107
- function _extractSubstitutionBodies(s) {
108
- if (typeof s !== 'string') return [];
109
- const out = [];
110
- const re = /\$\(([^()]*(?:\([^()]*\)[^()]*)*)\)|`([^`]*)`/g;
111
- let m;
112
- while ((m = re.exec(s)) !== null) {
113
- const body = m[1] != null ? m[1] : m[2];
114
- if (body && body.trim()) out.push(body);
115
- }
116
- return out;
117
- }
118
-
119
- // Combined injection-aware block targets: decoded form + substitution bodies
120
- // (and their decoded forms). Used on BOTH the persistent and stateless paths.
121
- export function _injectionBlockTargets(cmd) {
122
- return injectionBlockTargets(cmd);
123
- }
124
-
125
87
  function _prefixPowerShellUtf8(command) {
126
88
  const prefix = '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;';
127
89
  const text = String(command || '');
128
90
  return text.trimStart().startsWith(prefix) ? text : `${prefix}\n${text}`;
129
91
  }
130
92
 
131
- const _unquoteSpansForPolicy = (s) => s.replace(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g, (m) => m.slice(1, -1));
132
-
133
- // Same normalized + decoded target set as hard-block (strip/unquote/shell -c/PS).
134
- export { buildBashPolicyScanTargets } from '../bash-policy-scan.mjs';
135
-
136
93
  export function getDedupedDestructiveWarnings(command) {
137
94
  const seenMsg = new Set();
138
95
  const warnings = [];
@@ -343,7 +300,7 @@ export async function executeBashTool(args, workDir, options = {}) {
343
300
  // the background task finished (no polling tool is auto-driven). The
344
301
  // notify ctx is threaded down from the MCP dispatch frame
345
302
  // (server-main agentContext / _dispatchByModule) the same way the
346
- // bridge/explore-style tools receive notifyFn/routingSessionId/clientHostPid.
303
+ // agent/explore-style tools receive notifyFn/routingSessionId/clientHostPid.
347
304
  // Missing notifyFn (e.g. a non-MCP caller) degrades to a stderr
348
305
  // diagnostic inside watchBackgroundShellJob — never fails the spawn.
349
306
  try {
@@ -548,12 +505,12 @@ export async function executeTaskTool(args, options = {}) {
548
505
 
549
506
  const taskId = typeof args.task_id === 'string' ? args.task_id.trim() : '';
550
507
  if (!taskId) return 'Error: task_id is required';
551
- // bridge_* / sess_* are bridge-worker / orchestrator session ids, not
552
- // background shell jobs. task only resolves `shell mode=async`
553
- // jobs, so surface a self-correcting hint instead of the bare
554
- // "job not found" that otherwise invites a wrong-tool retry loop.
555
- if (/^(?:bridge_|sess_)/.test(taskId)) {
556
- return `Error: "${taskId}" is a bridge/session id, not a background task_id. Bridge agents deliver completion notifications; use bridge list/read only for manual recovery.`;
508
+ // sess_* values are agent/orchestrator session ids, not background shell
509
+ // tasks. task only resolves `shell mode=async` tasks, so surface a
510
+ // self-correcting hint instead of the bare "task not found" that otherwise
511
+ // invites a wrong-tool retry loop.
512
+ if (/^sess_/.test(taskId)) {
513
+ return `Error: "${taskId}" is an agent/session id, not a background task_id. Agent tasks deliver completion notifications; use agent list/read only for manual recovery.`;
557
514
  }
558
515
 
559
516
  const task = getBackgroundTask(taskId, { context: options });
@@ -17,14 +17,23 @@ export const BUILTIN_TOOLS = [
17
17
  name: 'read',
18
18
  title: 'Mixdog Read',
19
19
  annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
20
- description: 'Read a specific file window or symbol body after narrowing the target. Do not reread content already shown by code_graph or grep.',
20
+ description: 'Read known file path(s). Use line+context for small windows. Batch paths.',
21
21
  inputSchema: {
22
22
  type: 'object',
23
23
  properties: {
24
- path: { type: 'string', description: 'File path only; dirs use list.' },
25
- line: { type: 'number', minimum: 1, description: 'Anchor line. Do not combine with offset/limit.' },
26
- context: { type: 'number', minimum: 0, maximum: 200, description: 'Lines around line. Use only with line; max 200.' },
27
- symbol: { type: 'string', description: 'Symbol body via code graph.' },
24
+ path: {
25
+ anyOf: [
26
+ { type: 'string' },
27
+ {
28
+ type: 'array',
29
+ items: { type: 'string' },
30
+ minItems: 1,
31
+ },
32
+ ],
33
+ description: 'File path or array. Dirs use list.',
34
+ },
35
+ line: { type: 'number', minimum: 1, description: 'Anchor line.' },
36
+ context: { type: 'number', minimum: 0, maximum: 200, description: 'Lines around line; max 200.' },
28
37
  },
29
38
  },
30
39
  },
@@ -32,11 +41,11 @@ export const BUILTIN_TOOLS = [
32
41
  name: 'diagnostics',
33
42
  title: 'Mixdog Diagnostics',
34
43
  annotations: { title: 'Mixdog Diagnostics', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, compressible: true },
35
- description: 'Run the matching type/lint checker under a file or directory (tsc/eslint/ruff/etc.). Default cwd; no LSP.',
44
+ description: 'Run matching type/lint checker. Default cwd; no LSP.',
36
45
  inputSchema: {
37
46
  type: 'object',
38
47
  properties: {
39
- path: { type: 'string', description: 'File or directory to diagnose. Defaults to cwd.' },
48
+ path: { type: 'string', description: 'File/dir. Default cwd.' },
40
49
  },
41
50
  required: [],
42
51
  },
@@ -45,29 +54,29 @@ export const BUILTIN_TOOLS = [
45
54
  name: 'open_config',
46
55
  title: 'Open Config UI',
47
56
  annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
48
- description: 'Open the mixdog settings UI (Providers + Presets) in the browser. Starts the resident config server if needed and returns the UI URL. No params.',
57
+ description: 'Open settings UI; returns URL. No params.',
49
58
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
50
59
  },
51
60
  {
52
61
  name: 'shell',
53
62
  title: 'Mixdog Shell',
54
63
  annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
55
- description: `Run an OS shell command for git/build/test/run only. Do NOT use shell grep/cat/head for source; use code_graph/grep/read/list/glob. Set shell explicitly: 'powershell' for PowerShell or 'bash' for Git Bash/POSIX. Use cwd; omit empty workdir. Windows native shell does not support persistent/session_id. mode=async uses the shared background task system and returns task_id. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
64
+ description: `Run shell for git/build/test/run. Not for source browsing; use code_graph/grep/read/list/glob. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
56
65
  inputSchema: {
57
66
  type: 'object',
58
67
  properties: {
59
- command: { type: 'string', description: 'Shell command to run.' },
60
- cwd: { type: 'string', description: 'Working directory for the command; omit when using current cwd.' },
68
+ command: { type: 'string', description: 'Command.' },
69
+ cwd: { type: 'string', description: 'Working directory.' },
61
70
  workdir: { type: 'string', description: 'Alias for cwd; omit if empty.' },
62
- timeout: { type: 'number', description: 'Timeout in milliseconds.' },
63
- merge_stderr: { type: 'boolean', description: 'Merge stderr into stdout in the returned output.' },
71
+ timeout: { type: 'number', description: 'Timeout ms.' },
72
+ merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
64
73
  mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
65
74
  run_in_background: { type: 'boolean', description: `Legacy alias for mode=async. ${TOOL_ASYNC_EXECUTION_CONTRACT}` },
66
- persistent: { type: 'boolean', description: 'Persistent shell mode; unsupported on Windows native shell.' },
67
- session_id: { type: 'string', description: 'Persistent shell id; omit on Windows native shell.' },
68
- create: { type: 'boolean', description: 'Allow creating a new persistent session for an explicit session_id.' },
69
- close: { type: 'boolean', description: 'Close the persistent session after this command when supported.' },
70
- shell: { type: 'string', enum: ['bash', 'powershell'], description: "Force the shell. On Windows: 'bash' runs the command through Git Bash (POSIX syntax), 'powershell' forces PowerShell. On POSIX: 'powershell' resolves pwsh if installed (errors if absent); 'bash' is /bin/sh (already the default). Always set this explicitly; omitting uses the OS default (PowerShell on Windows), where POSIX syntax fails to parse." },
75
+ persistent: { type: 'boolean', description: 'Persistent shell mode.' },
76
+ session_id: { type: 'string', description: 'Persistent shell id.' },
77
+ create: { type: 'boolean', description: 'Create session_id if missing.' },
78
+ close: { type: 'boolean', description: 'Close persistent session.' },
79
+ shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows default is PowerShell; bash means Git Bash/POSIX.' },
71
80
  },
72
81
  required: ['command'],
73
82
  },
@@ -76,14 +85,14 @@ export const BUILTIN_TOOLS = [
76
85
  name: 'task',
77
86
  title: 'Background Task Control',
78
87
  annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
79
- description: `Manual control for an async background task by task_id. action: list | status | read | wait | cancel. ${TOOL_MANUAL_CONTROL_CONTRACT} Not for bridge_*/sess_* ids.`,
88
+ description: `Manual control for shell async task_id. actions: list/status/read/wait/cancel. ${TOOL_MANUAL_CONTROL_CONTRACT} Not sess_* or agent ids.`,
80
89
  inputSchema: {
81
90
  type: 'object',
82
91
  properties: {
83
- task_id: { type: 'string', description: 'task_id from shell mode=async. Use only for manual control; normal completion arrives as a notification.' },
84
- action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Manual control only: list all visible tasks; status is non-blocking; read includes available result/output; wait blocks inside this one call until done/timeout; cancel terminates when the task supports it. Do not call repeatedly as a polling loop.' },
85
- timeout_ms: { type: 'number', description: 'Maximum time for a single manual wait call. Prefer the completion notification instead of repeated waits.' },
86
- poll_ms: { type: 'number', description: 'Internal polling interval used only inside one manual wait call.' },
92
+ task_id: { type: 'string', description: 'shell async task_id.' },
93
+ action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Manual action. Avoid polling loops.' },
94
+ timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
95
+ poll_ms: { type: 'number', description: 'Wait poll ms.' },
87
96
  },
88
97
  required: [],
89
98
  },
@@ -92,7 +101,7 @@ export const BUILTIN_TOOLS = [
92
101
  name: 'grep',
93
102
  title: 'Mixdog Grep',
94
103
  annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
95
- description: 'Search text or regex within a known file or directory.',
104
+ description: 'Search text/logs/regex.',
96
105
  inputSchema: {
97
106
  type: 'object',
98
107
  properties: {
@@ -101,19 +110,19 @@ export const BUILTIN_TOOLS = [
101
110
  { type: 'string' },
102
111
  { type: 'array', items: { type: 'string' }, minItems: 1 },
103
112
  ],
104
- description: 'Literal/substring or ripgrep regex. Array = OR terms. Legacy `\\|` in a single string is accepted as OR.',
113
+ description: 'Text/regex. Array = OR.',
105
114
  },
106
- path: { type: 'string', description: 'One file or directory only; for many paths use common parent + glob.' },
115
+ path: { type: 'string', description: 'File or directory.' },
107
116
  glob: {
108
117
  anyOf: [
109
118
  { type: 'string' },
110
119
  { type: 'array', items: { type: 'string' }, minItems: 1 },
111
120
  ],
112
- description: 'Glob filter(s).',
121
+ description: 'Optional narrowing glob(s).',
113
122
  },
114
- output_mode: { type: 'string', enum: ['files_with_matches', 'content', 'count'], description: 'Default content.' },
115
- head_limit: { type: 'number', description: 'Max result lines.' },
116
- offset: { type: 'number', description: 'Skip N result lines for paging.' },
123
+ output_mode: { type: 'string', enum: ['files_with_matches', 'content', 'count'], description: 'Output mode.' },
124
+ head_limit: { type: 'number', description: 'Max lines.' },
125
+ offset: { type: 'number', description: 'Skip lines.' },
117
126
  },
118
127
  required: [],
119
128
  },
@@ -122,7 +131,7 @@ export const BUILTIN_TOOLS = [
122
131
  name: 'glob',
123
132
  title: 'Mixdog Glob',
124
133
  annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
125
- description: 'Find files by glob pattern. Use for broad file-space narrowing.',
134
+ description: 'Find files by glob.',
126
135
  inputSchema: {
127
136
  type: 'object',
128
137
  properties: {
@@ -131,33 +140,47 @@ export const BUILTIN_TOOLS = [
131
140
  { type: 'string' },
132
141
  { type: 'array', items: { type: 'string' }, minItems: 1 },
133
142
  ],
134
- description: 'Glob pattern, or array of patterns.',
143
+ description: 'Glob pattern(s).',
135
144
  },
136
145
  path: {
137
146
  anyOf: [
138
147
  { type: 'string' },
139
148
  { type: 'array', items: { type: 'string' }, minItems: 1 },
140
149
  ],
141
- description: 'Base directory, or array of base directories.',
150
+ description: 'Base directory/directories.',
142
151
  },
143
152
  head_limit: { type: 'number', description: 'Max entries.' },
144
- offset: { type: 'number', description: 'Skip N entries for paging.' },
153
+ offset: { type: 'number', description: 'Skip entries.' },
145
154
  },
146
155
  required: ['pattern'],
147
156
  },
148
157
  },
158
+ {
159
+ name: 'find',
160
+ title: 'Mixdog Find Files',
161
+ annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
162
+ description: 'Fuzzy-find files by partial path/name. Returns paths.',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ query: { type: 'string', description: 'Partial path/name words.' },
167
+ path: { type: 'string', description: 'Base directory.' },
168
+ head_limit: { type: 'number', description: 'Max paths.' },
169
+ },
170
+ required: ['query'],
171
+ },
172
+ },
149
173
  {
150
174
  name: 'list',
151
175
  title: 'Mixdog List Directory',
152
176
  annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
153
- description: 'List directory entries or fuzzy-rank filenames. Use for broad directory/file discovery.',
177
+ description: 'List known directory entries. Use glob for broad discovery.',
154
178
  inputSchema: {
155
179
  type: 'object',
156
180
  properties: {
157
- path: { type: 'string', description: 'Directory to list; omit for current cwd.' },
181
+ path: { type: 'string', description: 'Directory.' },
158
182
  head_limit: { type: 'number', description: 'Max entries.' },
159
183
  offset: { type: 'number', description: 'Skip N entries for paging.' },
160
- fuzzy: { type: 'string', description: 'Rank files by partial name.' },
161
184
  },
162
185
  required: [],
163
186
  },
@@ -5,6 +5,7 @@ import { deleteReadRangeIndexForPath } from './read-range-index.mjs';
5
5
  import { resolveAgainstCwd } from './path-utils.mjs';
6
6
 
7
7
  const RESULT_CACHE = new Map(); // key → { ts, value, paths, scopes, readSnapshotMeta, contentPrefixHash, bytes }
8
+ const RESULT_CACHE_INFLIGHT = new Map(); // key → Promise<value>
8
9
  const RESULT_CACHE_TTL_MS = 30_000;
9
10
  const RESULT_CACHE_MAX_ENTRIES = 200;
10
11
  const RESULT_CACHE_MAX_BYTES = (() => {
@@ -140,6 +141,22 @@ export function cacheSet(key, value, meta = {}) {
140
141
  }
141
142
  }
142
143
 
144
+ export async function runResultCacheInFlight(key, compute) {
145
+ const cached = cacheGet(key);
146
+ if (cached !== null) return cached;
147
+ const existing = RESULT_CACHE_INFLIGHT.get(key);
148
+ if (existing) return await existing;
149
+ const promise = Promise.resolve()
150
+ .then(() => compute())
151
+ .finally(() => {
152
+ if (RESULT_CACHE_INFLIGHT.get(key) === promise) {
153
+ RESULT_CACHE_INFLIGHT.delete(key);
154
+ }
155
+ });
156
+ RESULT_CACHE_INFLIGHT.set(key, promise);
157
+ return await promise;
158
+ }
159
+
143
160
  function rawContentCacheDelete(key) {
144
161
  const entry = RAW_CONTENT_CACHE.get(key);
145
162
  if (entry?.rawBuf) RAW_CONTENT_CACHE_BYTES = Math.max(0, RAW_CONTENT_CACHE_BYTES - entry.rawBuf.length);
@@ -230,7 +247,7 @@ export async function statPathsForMtime(paths, workDir, concurrency = 64, opts =
230
247
  // Hard per-stat deadline (0 = disabled, legacy behaviour). A hung stat
231
248
  // (dead mount / unresponsive network path) must not pin a worker forever;
232
249
  // on expiry the entry resolves to null (stat-failed) so glob's post-rg stat
233
- // phase is bounded instead of running to the 600s bridge watchdog.
250
+ // phase is bounded instead of running to the 600s agent watchdog.
234
251
  const deadlineMs = Number(opts.deadlineMs) > 0 ? Number(opts.deadlineMs) : 0;
235
252
  // Injectable stat impl for testing hung-FS behaviour deterministically.
236
253
  const statImpl = typeof opts._statImpl === 'function' ? opts._statImpl : fsPromises.stat;
@@ -293,7 +310,7 @@ export async function lstatPathsForMtime(paths, workDir, concurrency = 64, opts
293
310
  // Hard per-lstat deadline (0 = disabled, legacy behaviour). A hung lstat
294
311
  // (dead mount / unresponsive network path) must not pin a worker forever;
295
312
  // on expiry the entry resolves to null (stat-failed) so list's stat phase
296
- // is bounded instead of running to the 600s bridge watchdog.
313
+ // is bounded instead of running to the 600s agent watchdog.
297
314
  const deadlineMs = Number(opts.deadlineMs) > 0 ? Number(opts.deadlineMs) : 0;
298
315
  // Injectable lstat impl for testing hung-FS behaviour deterministically.
299
316
  const lstatImpl = typeof opts._lstatImpl === 'function' ? opts._lstatImpl : fsPromises.lstat;
@@ -342,6 +359,7 @@ export async function lstatPathsForMtime(paths, workDir, concurrency = 64, opts
342
359
 
343
360
  function cacheInvalidateAll() {
344
361
  RESULT_CACHE.clear();
362
+ RESULT_CACHE_INFLIGHT.clear();
345
363
  RESULT_CACHE_BYTES = 0;
346
364
  STAT_CACHE.clear();
347
365
  RAW_CONTENT_CACHE.clear();
@@ -356,6 +374,7 @@ function cacheInvalidatePaths(paths) {
356
374
  cacheInvalidateAll();
357
375
  return;
358
376
  }
377
+ RESULT_CACHE_INFLIGHT.clear();
359
378
  for (const [key, entry] of RESULT_CACHE) {
360
379
  if (cacheEntryOverlapsPaths(entry, affectedPaths)) {
361
380
  resultCacheDelete(key);
@@ -1,4 +1,4 @@
1
- // G6: device path block list (Claude Code parity). Reading these paths
1
+ // G6: device path block list (reference parity). Reading these paths
2
2
  // would either hang (waiting for stdin / tty) or produce infinite output
3
3
  // (/dev/zero, /dev/random). The device block list catches pseudo-files on
4
4
  // POSIX hosts that a user-allowed path can still hit.
@@ -282,11 +282,4 @@ async function _executeDiagnosticsImpl(args, workDir) {
282
282
  }
283
283
  }
284
284
 
285
- function _isDir(p, workDir) {
286
- try {
287
- const abs = isAbsolute(p) ? p : pathResolve(workDir, p);
288
- return statSync(abs).isDirectory();
289
- } catch { return false; }
290
- }
291
-
292
285
  export default executeDiagnosticsTool;
@@ -118,12 +118,10 @@ export function compileSimpleGlob(pattern) {
118
118
  // simpler and matches the noise patterns developers actually create.
119
119
  export const NOISE_DIR_NAMES = new Set([
120
120
  'node_modules', '.git',
121
- 'dist', 'build', 'out', 'target',
122
121
  '.next', '.nuxt', '.svelte-kit',
123
122
  '.cache', '.parcel-cache', '.turbo',
124
123
  'venv', '.venv', '__pycache__', '.pytest_cache',
125
- '.gradle', 'coverage',
126
- 'refs',
124
+ '.gradle',
127
125
  ]);
128
126
 
129
127
  export const MAX_WALK_ENTRIES = 200_000;
@@ -60,7 +60,7 @@ function listGuardPath(p) {
60
60
 
61
61
  export async function executeListTool(args, workDir, options = {}) {
62
62
  if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
63
- return executeFuzzyFind(args, workDir);
63
+ return executeFuzzyFindTool({ ...args, query: args.fuzzy }, workDir, options);
64
64
  }
65
65
  if (args.mode === 'tree') return executeTreeTool(args, workDir, options);
66
66
  if (args.mode === 'find') return executeFindFilesTool(args, workDir, options);
@@ -296,12 +296,13 @@ export async function executeTreeTool(args, workDir, options = {}) {
296
296
  return out;
297
297
  }
298
298
 
299
- // Fuzzy filename search (codex file-search / nucleo style): collect the file
300
- // list via `rg --files`, then rank by subsequence score. Lets a partial name
301
- // like "edeng" surface matching files in one call instead of guessing an exact
302
- // glob pattern. Honors path/hidden/include_noise/depth/head_limit.
303
- async function executeFuzzyFind(args, workDir) {
304
- const query = String(args.fuzzy);
299
+ // Fuzzy filename search (nucleo-style): collect the file
300
+ // list via `rg --files`, then rank by subsequence score. `list.fuzzy` still
301
+ // routes here for hidden backward compatibility, but the model-facing tool is
302
+ // `find`.
303
+ export async function executeFuzzyFindTool(args, workDir, options = {}) {
304
+ const query = String(args.query ?? args.fuzzy ?? '').trim();
305
+ if (!query) return 'Error: find requires query.';
305
306
  const inputPath = normalizeInputPath(args.path) || '.';
306
307
  const guard = listGuardPath(inputPath);
307
308
  if (guard) return guard;
@@ -310,10 +311,25 @@ async function executeFuzzyFind(args, workDir) {
310
311
  if (guardFull) return guardFull;
311
312
  const hidden = Boolean(args.hidden);
312
313
  const includeNoise = Boolean(args.include_noise);
313
- // head_limit:0 means "no cap" per list semantics keep 0 distinct from default.
314
- const headLimit = normalizeListHeadLimit(args.head_limit, 40);
314
+ // head_limit:0 means "no cap" per list semantics; default is intentionally
315
+ // compact so ambiguous discovery does not dump a huge candidate list.
316
+ const headLimit = normalizeListHeadLimit(args.head_limit, 25);
315
317
  const depth = args.depth != null ? Math.max(parseInt(args.depth, 10) || 1, 1) : null;
316
- const rgArgs = ['--files', '--no-ignore'];
318
+ const cacheKey = buildListCacheKey({
319
+ mode: 'fuzzy_find',
320
+ inputPath: normalizeOutputPath(fullPath),
321
+ depth: depth ?? '',
322
+ hidden,
323
+ sort: 'score',
324
+ typeFilter: 'file',
325
+ headLimit,
326
+ offset: '',
327
+ namePattern: query,
328
+ includeNoise,
329
+ });
330
+ const cached = cacheGet(cacheKey);
331
+ if (cached !== null) return cached;
332
+ const rgArgs = ['--files'];
317
333
  if (hidden) rgArgs.push('--hidden');
318
334
  if (depth != null) rgArgs.push('--max-depth', String(depth));
319
335
  if (!includeNoise) {
@@ -333,12 +349,20 @@ async function executeFuzzyFind(args, workDir) {
333
349
  .map((p) => (p.endsWith('\r') ? p.slice(0, -1) : p))
334
350
  .filter((p) => p.length > 0)
335
351
  .map((p) => ({ path: normalizeOutputPath(p.replace(/^\.[/\\]/, '')) }));
336
- const ranked = fuzzyRank(query, items, headLimit);
352
+ const rankLimit = headLimit > 0 ? headLimit + 1 : headLimit;
353
+ const rankedRaw = fuzzyRank(query, items, rankLimit);
354
+ const hasMore = headLimit > 0 && rankedRaw.length > headLimit;
355
+ const ranked = hasMore ? rankedRaw.slice(0, headLimit) : rankedRaw;
337
356
  if (ranked.length === 0) return `(no fuzzy match for "${query}")`;
338
357
  const out = ranked.map((r) => r.item.path).join('\n');
339
- return headLimit > 0 && ranked.length >= headLimit
358
+ const result = hasMore
340
359
  ? `${out}\n... (top ${headLimit}; raise head_limit for more)`
341
360
  : out;
361
+ cacheSet(cacheKey, result, { scopes: [fullPath] });
362
+ if (typeof options?.onProgress === 'function') {
363
+ try { options.onProgress(`${ranked.length} candidates`); } catch { /* best-effort */ }
364
+ }
365
+ return result;
342
366
  }
343
367
 
344
368
  export async function executeFindFilesTool(args, workDir, options = {}) {