mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -85,7 +85,7 @@ function isStringOrStringArray(v) {
85
85
  return true;
86
86
  }
87
87
 
88
- function hasUnsupportedRipgrepRegex(value) {
88
+ export function hasUnsupportedRipgrepRegex(value) {
89
89
  const values = Array.isArray(value) ? value : [value];
90
90
  return values.some((item) => {
91
91
  if (typeof item !== 'string') return false;
@@ -99,6 +99,30 @@ function hasMultipleAbsoluteWindowsPaths(value) {
99
99
  return Array.isArray(matches) && matches.length > 1;
100
100
  }
101
101
 
102
+ // Rescue for hasMultipleAbsoluteWindowsPaths: a single string arg that packs
103
+ // multiple absolute Windows paths together (space/comma/semicolon separated,
104
+ // e.g. "C:\a\b C:\c\d" or "C:\a\b, C:\c\d") is a common model mistake meaning
105
+ // two separate search roots, not one malformed path. Split on each drive-
106
+ // letter boundary and trim separator debris instead of hard-erroring;
107
+ // executeGrepTool's own array fan-out (search-tool.mjs) already knows how to
108
+ // run one grep per path and join the results. Returns null when the value
109
+ // does not cleanly resolve to 2+ non-empty segments.
110
+ function splitMultipleAbsoluteWindowsPaths(value) {
111
+ if (typeof value !== 'string') return null;
112
+ const starts = [];
113
+ const re = /[A-Za-z]:[\\/]/g;
114
+ let m;
115
+ while ((m = re.exec(value)) !== null) starts.push(m.index);
116
+ if (starts.length < 2) return null;
117
+ const parts = [];
118
+ for (let i = 0; i < starts.length; i++) {
119
+ const end = i + 1 < starts.length ? starts[i + 1] : value.length;
120
+ const part = value.slice(starts[i], end).trim().replace(/[,;]+$/, '').trim();
121
+ if (part) parts.push(part);
122
+ }
123
+ return parts.length > 1 ? parts : null;
124
+ }
125
+
102
126
  function hasWindowsDrivePath(value) {
103
127
  return typeof value === 'string' && /[A-Za-z]:[\\/]/.test(value);
104
128
  }
@@ -124,9 +148,15 @@ function coerceIntegerString(v) {
124
148
  return n;
125
149
  }
126
150
 
151
+ // Range-shaped strings ("1,40", "1-40", "1:40") are a common model mistake
152
+ // when trying to pass a line range through a single-integer field. They are
153
+ // not coerced (ambiguous which bound is intended); instead the rejection
154
+ // tells the model exactly what to do instead.
155
+ const RANGE_SHAPED_INT_STRING = /^\d+\s*[,\-:]\s*\d+$/;
156
+
127
157
  // Mutates a[field] in place when it is a lossless integer string, then
128
158
  // validates the (possibly coerced) value against [min, max].
129
- function checkIntInRange(a, field, min, max) {
159
+ function checkIntInRange(a, field, min, max, opts = {}) {
130
160
  let value = a[field];
131
161
  if (value === undefined || value === null) return null;
132
162
  if (typeof value === 'string') {
@@ -136,8 +166,20 @@ function checkIntInRange(a, field, min, max) {
136
166
  a[field] = coerced;
137
167
  }
138
168
  }
169
+ if (typeof value === 'string' && RANGE_SHAPED_INT_STRING.test(value.trim())) {
170
+ return `Error: builtin arg "${field}" takes a single integer; for a line range use offset+limit as separate integers or "path#L<start>-L<end>" in the path arg (got "${value}")`;
171
+ }
139
172
  if (!isFiniteInt(value)) {
140
- return `Error: builtin arg "${field}" must be a finite integer (got ${describeType(value)})`;
173
+ return `Error: builtin arg "${field}" must be a finite integer (got ${describeType(value)}); expected a single integer like 40`;
174
+ }
175
+ // Soft-cap fields (grep -A/-B/-C/context): silently clamp out-of-range
176
+ // instead of erroring — a model-guessed "300" for a 200-line cap is not
177
+ // a shape violation worth a retry turn, just clamp to the boundary.
178
+ if (opts.clamp) {
179
+ if (value < min) { a[field] = min; return null; }
180
+ if (value > max) { a[field] = max; return null; }
181
+ a[field] = value;
182
+ return null;
141
183
  }
142
184
  if (value < min) {
143
185
  return `Error: builtin arg "${field}" must be >= ${min} (got ${value})`;
@@ -216,10 +258,11 @@ function guardGrep(a) {
216
258
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
217
259
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
218
260
  }
219
- if (hasOwn(a, k) && hasUnsupportedRipgrepRegex(a[k])) {
220
- return `Error: grep arg "${k}" uses regex syntax ripgrep does not support here (lookaround/backrefs). Use plain pattern arrays or simpler regex.`;
221
- }
222
261
  }
262
+ // Lookaround/backreference patterns are no longer hard-rejected here:
263
+ // executeGrepTool (search-tool.mjs) detects them at runtime and routes to
264
+ // rg's PCRE2 engine (-P/--pcre2) when the installed rg build supports it,
265
+ // falling back to this same error text only when PCRE2 is unavailable.
223
266
  for (const k of globKeys) {
224
267
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
225
268
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
@@ -231,7 +274,11 @@ function guardGrep(a) {
231
274
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
232
275
  }
233
276
  if (hasOwn(a, k) && hasMultipleAbsoluteWindowsPaths(a[k])) {
234
- return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
277
+ const split = splitMultipleAbsoluteWindowsPaths(a[k]);
278
+ if (!split) {
279
+ return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
280
+ }
281
+ a[k] = split;
235
282
  }
236
283
  }
237
284
  for (const k of ['head_limit', 'offset']) {
@@ -239,7 +286,7 @@ function guardGrep(a) {
239
286
  if (err) return err;
240
287
  }
241
288
  for (const k of ['-A', '-B', '-C', 'context']) {
242
- const err = checkIntInRange(a, k, 0, GREP_CONTEXT_MAX);
289
+ const err = checkIntInRange(a, k, 0, GREP_CONTEXT_MAX, { clamp: true });
243
290
  if (err) return err;
244
291
  }
245
292
  // output_mode / mode enum
@@ -291,13 +338,19 @@ function applyLineContextWindow(obj, labelPrefix) {
291
338
  if (!hasLine) {
292
339
  return `Error: read arg "${labelPrefix}context" requires "${labelPrefix}line" to compute a window`;
293
340
  }
341
+ if (typeof obj.line === 'string' && RANGE_SHAPED_INT_STRING.test(obj.line.trim())) {
342
+ return `Error: read arg "${labelPrefix}line" takes a single integer; for a line range use offset+limit as separate integers or "path#L<start>-L<end>" in the path arg (got "${obj.line}")`;
343
+ }
294
344
  if (!isFiniteInt(obj.line) || obj.line < 1) {
295
- return `Error: read arg "${labelPrefix}line" must be a finite integer >= 1 (got ${describeType(obj.line)})`;
345
+ return `Error: read arg "${labelPrefix}line" must be a finite integer >= 1 (got ${describeType(obj.line)}); expected a single integer like 40`;
296
346
  }
297
347
  let ctx = 0;
298
348
  if (hasContext) {
349
+ if (typeof obj.context === 'string' && RANGE_SHAPED_INT_STRING.test(obj.context.trim())) {
350
+ return `Error: read arg "${labelPrefix}context" takes a single integer; for a line range use offset+limit as separate integers or "path#L<start>-L<end>" in the path arg (got "${obj.context}")`;
351
+ }
299
352
  if (!isFiniteInt(obj.context) || obj.context < 0) {
300
- return `Error: read arg "${labelPrefix}context" must be a non-negative finite integer (got ${describeType(obj.context)})`;
353
+ return `Error: read arg "${labelPrefix}context" must be a non-negative finite integer (got ${describeType(obj.context)}); expected a single integer like 40`;
301
354
  }
302
355
  ctx = obj.context;
303
356
  }
@@ -381,13 +434,6 @@ function guardRead(a) {
381
434
  return null;
382
435
  }
383
436
 
384
- function guardDiagnostics(a) {
385
- if (hasOwn(a, 'path') && a.path !== undefined && a.path !== null && !isNonEmptyString(a.path)) {
386
- return `Error: diagnostics arg "path" must be a non-empty string (got ${describeType(a.path)})`;
387
- }
388
- return null;
389
- }
390
-
391
437
  function guardShell(a) {
392
438
  if (!hasOwn(a, 'command')) {
393
439
  return 'Error: shell requires "command"';
@@ -458,8 +504,13 @@ function guardList(a) {
458
504
  }
459
505
 
460
506
  function guardFind(a) {
461
- if (!hasOwn(a, 'query') || typeof a.query !== 'string' || a.query.trim().length === 0) {
462
- return `Error: find requires non-empty string "query" (got ${describeType(a.query)})`;
507
+ // query accepts string or string[] (query[] fans out per-lookup downstream).
508
+ const queryOk = hasOwn(a, 'query') && (
509
+ (typeof a.query === 'string' && a.query.trim().length > 0)
510
+ || (Array.isArray(a.query) && a.query.length > 0 && a.query.every((q) => typeof q === 'string' && q.trim().length > 0))
511
+ );
512
+ if (!queryOk) {
513
+ return `Error: find requires non-empty string (or string[]) "query" (got ${describeType(a.query)})`;
463
514
  }
464
515
  if (hasOwn(a, 'path') && !isString(a.path)) {
465
516
  return `Error: find arg "path" must be a string (got ${describeType(a.path)})`;
@@ -541,7 +592,6 @@ function guardCodeGraph(a) {
541
592
  const GUARDS = {
542
593
  grep: guardGrep,
543
594
  read: guardRead,
544
- diagnostics: guardDiagnostics,
545
595
  shell: guardShell,
546
596
  task: guardTask,
547
597
  list: guardList,
@@ -39,6 +39,7 @@ import { normalizeErrorMessage } from './path-diagnostics.mjs';
39
39
  import { invalidateBuiltinResultCache } from './cache-layers.mjs';
40
40
  import { resolveOptionalCwd } from './cwd-utils.mjs';
41
41
  import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
42
+ import { resolveSessionCwd, stateFilePath, wrapPowerShellWithCwdProbe, wrapBashWithCwdProbe } from '../shell-state.mjs';
42
43
 
43
44
  // Post-exec drift detection. After a foreground shell command, compare the
44
45
  // live mtime+size of files mixdog has already read this session against their
@@ -113,7 +114,12 @@ export async function executeBashTool(args, workDir, options = {}) {
113
114
  const requestedCwd = args.cwd ?? args.workdir;
114
115
  const cwdResult = resolveOptionalCwd(requestedCwd, workDir);
115
116
  if (cwdResult.error) return cwdResult.error;
116
- const bashWorkDir = cwdResult.cwd;
117
+ // Session cwd carry-over (Claude Code style, no live shell): when the model
118
+ // passes an explicit cwd it wins and updates the store on the next probe;
119
+ // otherwise reuse the last stored cwd for this session if it still exists.
120
+ const _hasExplicitCwd = typeof requestedCwd === 'string' && requestedCwd.trim() !== '';
121
+ const _sessionCwdKey = options?.sessionId ?? options?.readStateScope ?? options?.callerSessionId ?? null;
122
+ const bashWorkDir = resolveSessionCwd(_sessionCwdKey, _hasExplicitCwd ? cwdResult.cwd : null, cwdResult.cwd);
117
123
  const _readStateScope = options?.readStateScope ?? options?.sessionId ?? null;
118
124
  const executionMode = resolveExecutionMode(args || {}, args?.run_in_background === true ? 'async' : 'sync');
119
125
  const runInBackground = executionMode === 'async';
@@ -318,8 +324,22 @@ export async function executeBashTool(args, workDir, options = {}) {
318
324
  try { bashAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
319
325
  catch { bashAbortSignal = null; }
320
326
  const combinedBashAbort = _combineAbortSignals(bashAbortSignal, options?.abortSignal || null);
327
+ // Sync path only: chain a trailing cwd probe so the session's final
328
+ // working directory persists to the next shell call. Async jobs run
329
+ // detached and are intentionally excluded (they never reach here). The
330
+ // probe captures the command's exit status first and re-exits with it,
331
+ // so the exit code the model sees is unchanged.
332
+ let syncCommand = wrappedCommand;
333
+ try {
334
+ const _stateFile = stateFilePath(_sessionCwdKey);
335
+ if (_stateFile) {
336
+ syncCommand = (process.platform === 'win32' && shellType === 'powershell')
337
+ ? wrapPowerShellWithCwdProbe(wrappedCommand, _stateFile)
338
+ : wrapBashWithCwdProbe(wrappedCommand, _stateFile);
339
+ }
340
+ } catch { syncCommand = wrappedCommand; }
321
341
  const result = await execShellCommand({
322
- shell, shellArg, shellArgs, command: wrappedCommand,
342
+ shell, shellArg, shellArgs, command: syncCommand,
323
343
  env: spawnEnv,
324
344
  cwd: bashWorkDir,
325
345
  timeoutMs: timeout,
@@ -17,7 +17,7 @@ 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 known file path(s). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit only. Batch paths/regions as real arrays; adjacent spans in one file = one window, not repeated calls. Dirs use list.',
20
+ description: 'Read known file path(s). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit. Batch paths/regions as real arrays; adjacent spans in one file = one window, not repeated calls. Dirs use list.',
21
21
  inputSchema: {
22
22
  type: 'object',
23
23
  properties: {
@@ -42,56 +42,27 @@ export const BUILTIN_TOOLS = [
42
42
  minItems: 1,
43
43
  },
44
44
  ],
45
- description: 'File path, path[], or {path,offset,limit}[] region objects. Pass arrays directly; JSON strings are legacy recovery only.',
45
+ description: 'File path. Batch spans via path[] or {path,offset,limit}[] regions. Pass arrays directly; JSON strings are legacy recovery only.',
46
46
  },
47
47
  offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip before reading; 0 starts at line 1. Continue with offset:N.' },
48
48
  limit: { type: 'number', minimum: 1, description: 'Numeric max lines to return after offset.' },
49
49
  },
50
50
  },
51
51
  },
52
- {
53
- name: 'diagnostics',
54
- title: 'Mixdog Diagnostics',
55
- annotations: { title: 'Mixdog Diagnostics', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, compressible: true },
56
- description: 'Run matching type/lint checker. Default cwd; no LSP.',
57
- inputSchema: {
58
- type: 'object',
59
- properties: {
60
- path: { type: 'string', description: 'File/dir. Default cwd.' },
61
- },
62
- required: [],
63
- },
64
- },
65
- {
66
- name: 'open_config',
67
- title: 'Open Config UI',
68
- // agentHidden: a worker/reviewer session has no business popping the
69
- // settings UI on the user's machine; it also wastes schema bytes on
70
- // every agent request. Lead keeps it.
71
- annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, agentHidden: true },
72
- description: 'Open settings UI; returns URL. No params.',
73
- inputSchema: { type: 'object', properties: {}, additionalProperties: false },
74
- },
75
52
  {
76
53
  name: 'shell',
77
54
  title: 'Mixdog Shell',
78
55
  annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
79
- description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching, or checking existence go through the dedicated tools, never a shell command. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
56
+ description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching, existence checks use the dedicated tools, never shell. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
80
57
  inputSchema: {
81
58
  type: 'object',
82
59
  properties: {
83
60
  command: { type: 'string', description: 'Command.' },
84
- cwd: { type: 'string', description: 'Working directory.' },
85
- workdir: { type: 'string', description: 'Alias for cwd; omit if empty.' },
61
+ cwd: { type: 'string', description: 'Working directory. Persists across shell calls in a session — omit to reuse the previous command\'s directory (e.g. after cd); pass an absolute path to change it.' },
86
62
  timeout: { type: 'number', description: 'Timeout ms.' },
87
63
  merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
88
64
  mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
89
- run_in_background: { type: 'boolean', description: `Legacy alias for mode=async. ${TOOL_ASYNC_EXECUTION_CONTRACT}` },
90
- persistent: { type: 'boolean', description: 'Persistent shell mode.' },
91
- session_id: { type: 'string', description: 'Persistent shell id.' },
92
- create: { type: 'boolean', description: 'Create session_id if missing.' },
93
- close: { type: 'boolean', description: 'Close persistent session.' },
94
- shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows default is PowerShell; bash means Git Bash/POSIX.' },
65
+ shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
95
66
  },
96
67
  required: ['command'],
97
68
  },
@@ -116,7 +87,7 @@ export const BUILTIN_TOOLS = [
116
87
  name: 'grep',
117
88
  title: 'Mixdog Grep',
118
89
  annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
119
- description: 'Search file contents by text/regex in a known scope. Use files_with_matches/count for broad anchors, content_with_context for narrow code answers. One concept → one grep.',
90
+ description: 'Search file contents by text/regex in a known scope. files_with_matches/count for broad anchors, content_with_context for narrow answers. One concept → one grep; multiple scopes = ONE call with path[].',
120
91
  inputSchema: {
121
92
  type: 'object',
122
93
  properties: {
@@ -127,7 +98,13 @@ export const BUILTIN_TOOLS = [
127
98
  ],
128
99
  description: 'Text/regex. Put synonyms in pattern[] as OR in ONE grep; no serial rewording or equivalent repeats.',
129
100
  },
130
- path: { type: 'string', description: 'Known narrowest file/dir; broad scopes return paths first, then refine from returned paths.' },
101
+ path: {
102
+ anyOf: [
103
+ { type: 'string' },
104
+ { type: 'array', items: { type: 'string' }, minItems: 1 },
105
+ ],
106
+ description: 'Known narrowest file/dir, or path[] to search several scopes in one call; broad scopes return paths first, then refine from returned paths.',
107
+ },
131
108
  glob: {
132
109
  anyOf: [
133
110
  { type: 'string' },
@@ -136,9 +113,11 @@ export const BUILTIN_TOOLS = [
136
113
  description: 'Narrow in same grep; no follow-up grep for equivalent scope changes.',
137
114
  },
138
115
  output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad scope: files_with_matches/count. Narrow scope: content_with_context; answer from it, skip read unless span is not shown.' },
139
- context: { type: 'number', minimum: 0, description: 'Lines before/after each match; keep bounded.' },
140
116
  head_limit: { type: 'number', minimum: 0, description: 'Max output lines; keep small.' },
141
117
  offset: { type: 'number', minimum: 0, description: 'Skip output lines for paging.' },
118
+ '-A': { type: 'number', minimum: 0, description: 'Lines after each match.' },
119
+ '-B': { type: 'number', minimum: 0, description: 'Lines before each match.' },
120
+ '-C': { type: 'number', minimum: 0, description: 'Lines before/after each match.' },
142
121
  },
143
122
  required: [],
144
123
  },
@@ -147,7 +126,7 @@ export const BUILTIN_TOOLS = [
147
126
  name: 'glob',
148
127
  title: 'Mixdog Glob',
149
128
  annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
150
- description: 'Find files by exact glob. Unknown path/name uses find.',
129
+ description: 'Find files by exact glob. Unknown path/name uses find. Multiple patterns/dirs = ONE call with pattern[]/path[], never parallel single-pattern calls.',
151
130
  inputSchema: {
152
131
  type: 'object',
153
132
  properties: {
@@ -156,14 +135,14 @@ export const BUILTIN_TOOLS = [
156
135
  { type: 'string' },
157
136
  { type: 'array', items: { type: 'string' }, minItems: 1 },
158
137
  ],
159
- description: 'Exact glob pattern(s).',
138
+ description: 'Exact glob pattern(s). Batch related patterns as pattern[] in one call.',
160
139
  },
161
140
  path: {
162
141
  anyOf: [
163
142
  { type: 'string' },
164
143
  { type: 'array', items: { type: 'string' }, minItems: 1 },
165
144
  ],
166
- description: 'Base directory/directories.',
145
+ description: 'Base directory/directories. Batch multiple roots as path[] in one call.',
167
146
  },
168
147
  head_limit: { type: 'number', description: 'Max entries.' },
169
148
  offset: { type: 'number', description: 'Skip entries.' },
@@ -175,11 +154,17 @@ export const BUILTIN_TOOLS = [
175
154
  name: 'find',
176
155
  title: 'Mixdog Find Files',
177
156
  annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
178
- description: 'Find files by partial path/name. Exact structure uses glob. Returns verified paths.',
157
+ description: 'Find files by partial path/name. Exact structure uses glob. Returns verified paths. Multiple names = ONE call with query[].',
179
158
  inputSchema: {
180
159
  type: 'object',
181
160
  properties: {
182
- query: { type: 'string', description: 'Partial path/name words; not file contents.' },
161
+ query: {
162
+ anyOf: [
163
+ { type: 'string' },
164
+ { type: 'array', items: { type: 'string' }, minItems: 1 },
165
+ ],
166
+ description: 'Partial path/name words (not file contents), or query[] to batch lookups in one call.',
167
+ },
183
168
  path: { type: 'string', description: 'Base directory.' },
184
169
  head_limit: { type: 'number', description: 'Max paths.' },
185
170
  },
@@ -190,11 +175,17 @@ export const BUILTIN_TOOLS = [
190
175
  name: 'list',
191
176
  title: 'Mixdog List Directory',
192
177
  annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
193
- description: 'List known directory entries. Use glob for broad discovery.',
178
+ description: 'List known directory entries. Use glob for broad discovery. Multiple dirs = ONE call with path[].',
194
179
  inputSchema: {
195
180
  type: 'object',
196
181
  properties: {
197
- path: { type: 'string', description: 'Directory.' },
182
+ path: {
183
+ anyOf: [
184
+ { type: 'string' },
185
+ { type: 'array', items: { type: 'string' }, minItems: 1 },
186
+ ],
187
+ description: 'Directory, or path[] to list several directories in one call.',
188
+ },
198
189
  head_limit: { type: 'number', description: 'Max entries.' },
199
190
  offset: { type: 'number', description: 'Skip N entries for paging.' },
200
191
  },
@@ -60,6 +60,26 @@ function listGuardPath(p) {
60
60
  }
61
61
 
62
62
  export async function executeListTool(args, workDir, options = {}) {
63
+ if (Array.isArray(args.path)) {
64
+ const list = [...new Set(args.path.map((p) => (typeof p === 'string' ? p.trim() : '')).filter((p) => p.length > 0))];
65
+ const capped = list.length > 10;
66
+ const targets = capped ? list.slice(0, 10) : list;
67
+ if (targets.length > 1) {
68
+ const sections = [];
69
+ for (const p of targets) {
70
+ let body;
71
+ try {
72
+ body = await executeListTool({ ...args, path: p }, workDir, options);
73
+ } catch (err) {
74
+ body = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
75
+ }
76
+ sections.push(`# list ${p}\n${body}`);
77
+ }
78
+ if (capped) sections.push(`... [capped at 10 of ${list.length} paths]`);
79
+ return sections.join('\n\n');
80
+ }
81
+ args.path = targets[0];
82
+ }
63
83
  if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
64
84
  return executeFuzzyFindTool({ ...args, query: args.fuzzy }, workDir, options);
65
85
  }
@@ -302,6 +322,26 @@ export async function executeTreeTool(args, workDir, options = {}) {
302
322
  // routes here for hidden backward compatibility, but the model-facing tool is
303
323
  // `find`.
304
324
  export async function executeFuzzyFindTool(args, workDir, options = {}) {
325
+ if (Array.isArray(args.query)) {
326
+ const list = [...new Set(args.query.map((q) => (typeof q === 'string' ? q.trim() : '')).filter((q) => q.length > 0))];
327
+ const capped = list.length > 10;
328
+ const targets = capped ? list.slice(0, 10) : list;
329
+ if (targets.length > 1) {
330
+ const sections = [];
331
+ for (const q of targets) {
332
+ let body;
333
+ try {
334
+ body = await executeFuzzyFindTool({ ...args, query: q }, workDir, options);
335
+ } catch (err) {
336
+ body = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
337
+ }
338
+ sections.push(`# find ${q}\n${body}`);
339
+ }
340
+ if (capped) sections.push(`... [capped at 10 of ${list.length} queries]`);
341
+ return sections.join('\n\n');
342
+ }
343
+ args.query = targets[0];
344
+ }
305
345
  const query = String(args.query ?? args.fuzzy ?? '').trim();
306
346
  if (!query) return 'Error: find requires query.';
307
347
  const inputPath = normalizeInputPath(args.path) || '.';
@@ -181,6 +181,35 @@ async function assertRgAvailable() {
181
181
  throw e;
182
182
  }
183
183
 
184
+ // ── PCRE2 capability detection ───────────────────────────────────────────
185
+ // ripgrep's default regex engine (Rust `regex` crate) intentionally rejects
186
+ // lookaround/backreferences ("look-around ... is not supported" /
187
+ // "backreferences ... not supported"). Some rg builds are compiled with the
188
+ // optional `pcre2` feature, which adds `-P`/`--pcre2` support for exactly
189
+ // those constructs. Detect capability once via `rg --pcre2-version`: it
190
+ // exits 0 and prints a version when PCRE2 is linked in, and exits non-zero
191
+ // ("PCRE2 is not available...") when it is not. Cached permanently on
192
+ // success; failure is cached briefly so a mid-session rg upgrade/reinstall
193
+ // is picked up without a daemon restart (mirrors rgFallbackUsable's TTL).
194
+ const RG_PCRE2_FAIL_TTL_MS = 30000;
195
+ let _rgPcre2Supported = null;
196
+ let _rgPcre2FailAt = 0;
197
+ export async function rgSupportsPcre2() {
198
+ if (_rgPcre2Supported === true) return true;
199
+ if (_rgPcre2Supported === false && (Date.now() - _rgPcre2FailAt) < RG_PCRE2_FAIL_TTL_MS) {
200
+ return false;
201
+ }
202
+ try {
203
+ await ensureRgResolved();
204
+ await execFileAsync(rgExecutable(), ['--pcre2-version'], { windowsHide: true, timeout: 3000 });
205
+ _rgPcre2Supported = true;
206
+ } catch {
207
+ _rgPcre2Supported = false;
208
+ _rgPcre2FailAt = Date.now();
209
+ }
210
+ return _rgPcre2Supported;
211
+ }
212
+
184
213
  // Cap rg stdout accumulation so a runaway producer (huge repo, accidental
185
214
  // match-all) cannot balloon the JS string heap. Mirrors CC's ripgrep.ts cap.
186
215
  const MAX_RG_STDOUT_BYTES = 20 * 1024 * 1024; // 20MB
@@ -29,6 +29,7 @@ export function buildGrepCacheKey(parts) {
29
29
  multilineMode,
30
30
  fileType,
31
31
  onlyMatching,
32
+ pcre2,
32
33
  } = parts;
33
34
  return [
34
35
  'grep',
@@ -46,6 +47,7 @@ export function buildGrepCacheKey(parts) {
46
47
  multilineMode ? 'm1' : 'm0',
47
48
  onlyMatching ? 'o1' : 'o0',
48
49
  Array.isArray(fileType) ? fileType.join('\x01') : (fileType || ''),
50
+ pcre2 ? 'p1' : 'p0',
49
51
  ].join('|');
50
52
  }
51
53
 
@@ -64,6 +66,7 @@ export function buildGrepRgArgs(parts) {
64
66
  fileType,
65
67
  onlyMatching,
66
68
  fixedStrings = false,
69
+ pcre2 = false,
67
70
  } = parts;
68
71
  // `--hidden`: search dotfiles/dot-dirs (.github, .mixdog) that
69
72
  // rg skips by default. The DEFAULT_IGNORE_GLOBS below still exclude .git and
@@ -84,6 +87,11 @@ export function buildGrepRgArgs(parts) {
84
87
  }
85
88
  if (caseInsensitive) rgArgs.push('-i');
86
89
  if (fixedStrings) rgArgs.push('-F');
90
+ // PCRE2 engine: opt-in only when the caller already confirmed (via
91
+ // rgSupportsPcre2() in search-tool.mjs) that the installed rg binary was
92
+ // built with PCRE2 support. Enables lookaround/backreference patterns
93
+ // that the default Rust regex engine rejects outright.
94
+ if (pcre2) rgArgs.push('-P');
87
95
  if (multilineMode) rgArgs.push('-U', '--multiline-dotall');
88
96
  if (Array.isArray(fileType)) {
89
97
  for (const t of fileType) if (t) rgArgs.push('--type', t);
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Search path diagnostics: ENOENT recovery + path-shape helpers shared by
3
+ * grep/glob in search-tool.mjs. Extracted verbatim so search-tool.mjs stays a
4
+ * thin executor; search-tool.mjs re-exports these for unchanged importers.
5
+ */
6
+ import { statSync } from 'fs';
7
+ import { isAbsolute, resolve } from 'path';
8
+ import { findBySuffixStrip, findFileByBasename } from './path-diagnostics.mjs';
9
+ import { normalizeOutputPath, resolveAgainstCwd } from './path-utils.mjs';
10
+
11
+ // Deterministic ENOENT recovery: when a grep path does not exist, surface
12
+ // indexed files that share the missing path's basename, turning a guessed or
13
+ // misplaced path (e.g. session/result-compression.mjs vs the real
14
+ // tools/result-compression.mjs) into the actual file in one step. Exact
15
+ // basename only — no stem/token fuzzing — so the hint is high-signal and
16
+ // noise-free. Invariant: every ENOENT runs the same basename lookup; there is
17
+ // no "guessed a lot" branch. Returns '' (appends nothing) when no same-named
18
+ // indexed file exists or the glob child is unavailable.
19
+ export async function _suggestIndexedPaths(missingPath, executeChildBuiltinTool, workDir) {
20
+ if (typeof executeChildBuiltinTool !== 'function') return '';
21
+ const base = String(missingPath).replace(/\\/g, '/').split('/').pop();
22
+ // Skip when there is no usable basename or it carries glob magic (the
23
+ // pattern, not a literal filename, would not map to a real file).
24
+ if (!base || /[*?[\]{}]/.test(base)) return '';
25
+ try {
26
+ const out = await executeChildBuiltinTool('glob', { pattern: `**/${base}`, head_limit: 6 }, workDir);
27
+ if (typeof out !== 'string') return '';
28
+ // Drop ONLY the exact diagnostic forms glob emits: the empty-result line
29
+ // ("(no files found ...", "(no entries after offset ...") and the
30
+ // suffix/warning lines ("... [N more entries]", "... [warning] ...").
31
+ // A broad startsWith('(' / '...' / 'Error') would wrongly drop real
32
+ // paths like ErrorBoundary.mjs or a "(draft)/x.mjs" leading segment.
33
+ const hits = out.split('\n')
34
+ .filter((s) => s && !/^\(no (?:files found|entries\b)/.test(s) && !/^\.\.\. \[/.test(s))
35
+ .map((s) => s.trim())
36
+ .filter(Boolean)
37
+ .slice(0, 5);
38
+ return hits.length ? `\n[path not found here; same-named indexed file(s): ${hits.join(', ')}]` : '';
39
+ } catch {
40
+ return '';
41
+ }
42
+ }
43
+
44
+ // Models occasionally glue a quoted segment onto an otherwise-unquoted path
45
+ // (e.g. `C:/Project/mixdog/"src/x.mjs"` — prefix bare, trailing segment
46
+ // wrapped in literal quote chars from a mis-escaped interpolation). But
47
+ // quote chars CAN be legit path content (POSIX `src/Bob's/file.mjs`), so
48
+ // stripping is existence-guarded: keep the original if it stats, only fall
49
+ // back to the stripped variant otherwise. Cost is one statSync and only on
50
+ // the rare quote-containing path.
51
+ export function stripEmbeddedPathQuotes(p) {
52
+ if (typeof p !== 'string' || !p) return p;
53
+ if (!p.includes('"') && !p.includes("'")) return p;
54
+ try { statSync(p); return p; } catch { /* fall through to stripped */ }
55
+ return p.replace(/['"]/g, '');
56
+ }
57
+
58
+ // Reuse read's own ENOENT recovery (path-diagnostics findBySuffixStrip /
59
+ // findFileByBasename — the exact fs scans read-single-tool.mjs already runs
60
+ // for its "same filename exists at" hint) so a grep/glob path miss redirects
61
+ // to the real location instead of dead-ending. No new fs scan is added here;
62
+ // this only calls the two helpers read already calls, with a caller-supplied
63
+ // action verb ("Search"/"List") in place of read's "Read". Gated to genuine
64
+ // not-found codes: EACCES/EPERM etc. keep their real failure semantics and
65
+ // must not trigger same-basename guidance or the BFS scan.
66
+ const NOT_FOUND_CODES = new Set(['ENOENT', 'ENOTDIR']);
67
+ export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'ENOENT') {
68
+ if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return '';
69
+ const suffixHit = findBySuffixStrip(workDir, missingPath);
70
+ if (suffixHit) {
71
+ return ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". ${actionVerb} that path directly.`;
72
+ }
73
+ const elsewhere = findFileByBasename(workDir, missingPath);
74
+ if (elsewhere.length) {
75
+ return ` Not found at this path; the same filename exists at: ${elsewhere.map((p) => `"${normalizeOutputPath(p)}"`).join(', ')}. ${actionVerb} that path directly.`;
76
+ }
77
+ return '';
78
+ }
79
+
80
+ export function relativePathPrefix(pathPrefix, workDir) {
81
+ if (!workDir) return pathPrefix;
82
+ const cwdFwd = workDir.replace(/\\/g, '/').replace(/\/+$/, '');
83
+ const absFwd = String(pathPrefix || '').replace(/\\/g, '/');
84
+ const haystack = process.platform === 'win32' ? absFwd.toLocaleLowerCase() : absFwd;
85
+ const needle = process.platform === 'win32' ? cwdFwd.toLocaleLowerCase() : cwdFwd;
86
+ if (haystack.startsWith(needle + '/') || haystack === needle) {
87
+ return absFwd.slice(cwdFwd.length + 1) || '.';
88
+ }
89
+ return pathPrefix;
90
+ }
91
+
92
+ export function relativeSearchResultPath(path, workDir) {
93
+ const normalizedWorkDir = normalizeOutputPath(workDir);
94
+ const normalizedAbs = normalizeOutputPath(path);
95
+ if (normalizedAbs.startsWith(normalizedWorkDir + '/') || normalizedAbs.startsWith(normalizedWorkDir + '\\')) {
96
+ return normalizedAbs.slice(normalizedWorkDir.length + 1);
97
+ }
98
+ return normalizedAbs;
99
+ }
100
+
101
+ export function resolveSearchScope(root, workDir) {
102
+ return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
103
+ }
104
+
105
+ export function isUncOrSmbPath(path) {
106
+ if (typeof path !== 'string' || !path) return false;
107
+ return path.startsWith('\\\\') || path.startsWith('//');
108
+ }
109
+
110
+ export function uncRefusalMessage(toolName, original, resolved) {
111
+ const shown = normalizeOutputPath(resolved || original || '');
112
+ return `Error: ${toolName} refuses UNC/SMB path ${JSON.stringify(shown)}; remote share access is blocked to prevent NTLM credential leaks`;
113
+ }
114
+
115
+ export function basePathDiagnostic(basePaths, workDir) {
116
+ return basePaths.map((basePath) => {
117
+ const resolved = resolveSearchScope(basePath, workDir);
118
+ try {
119
+ const st = statSync(resolved);
120
+ return `${normalizeOutputPath(basePath)}: ${st.isDirectory() ? 'path exists (dir)' : 'path exists (file)'}`;
121
+ } catch (err) {
122
+ return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`
123
+ + buildNotFoundHint(workDir, resolved, 'Search', err?.code);
124
+ }
125
+ }).join('; ');
126
+ }